22 Commits
Author SHA1 Message Date
Muse 9e0926be96 Merge branch 'fix/docs-l-ui-install-uninstall'
Docs CI / build (push) Waiting to run
Docs Deploy (GitHub Pages) / build (push) Waiting to run
Docs Deploy (GitHub Pages) / deploy (push) Blocked by required conditions
Release 3X-UI / build (386) (push) Waiting to run
Release 3X-UI / build (amd64) (push) Waiting to run
Release 3X-UI / build (arm64) (push) Waiting to run
Release 3X-UI / build (armv5) (push) Waiting to run
Release 3X-UI / build (armv6) (push) Waiting to run
Release 3X-UI / build (armv7) (push) Waiting to run
Release 3X-UI / build (s390x) (push) Waiting to run
Release 3X-UI / Build for Windows (amd64) (push) Waiting to run
Release 3X-UI / Publish rolling dev release (push) Blocked by required conditions
2026-10-24 19:24:56 +00:00
Muse 0d0a4b32ae docs: rewrite install/uninstall instructions for l-ui (drop upstream 3x-ui commands) 2026-10-24 19:24:50 +00:00
Muse 9c6b594328 release: panel V2.7.5 (panel-only, agent line A2.6.0)
CI / go-test (push) Waiting to run
CI / postgres-durable-first (push) Waiting to run
CI / codegen (push) Waiting to run
CI / govulncheck (push) Waiting to run
CI / race (push) Waiting to run
CI / fuzz-smoke (push) Waiting to run
CI / golangci (push) Waiting to run
CI / frontend (push) Waiting to run
Release 3X-UI / build (386) (push) Waiting to run
Release 3X-UI / build (amd64) (push) Waiting to run
Release 3X-UI / build (arm64) (push) Waiting to run
Release 3X-UI / build (armv5) (push) Waiting to run
Release 3X-UI / build (armv6) (push) Waiting to run
Release 3X-UI / build (armv7) (push) Waiting to run
Release 3X-UI / build (s390x) (push) Waiting to run
Release 3X-UI / Build for Windows (amd64) (push) Waiting to run
Release 3X-UI / Publish rolling dev release (push) Blocked by required conditions
CodeQL Advanced / Analyze (go) (push) Waiting to run
CodeQL Advanced / Analyze (actions) (push) Waiting to run
CodeQL Advanced / Analyze (javascript-typescript) (push) Waiting to run
2026-10-24 18:58:51 +00:00
Muse 4000be75e0 Merge branch 'fix/probe-meta-date-row' 2026-10-24 18:58:39 +00:00
Muse ad3e70d59b fix(nodes): probe meta date picker and single-row date/traffic fields 2026-10-24 18:56:48 +00:00
Muse f45a6f626f release: panel V2.7.4 (panel-only, agent line A2.6.0)
CI / go-test (push) Waiting to run
CI / postgres-durable-first (push) Waiting to run
CI / codegen (push) Waiting to run
CI / govulncheck (push) Waiting to run
CI / race (push) Waiting to run
CI / fuzz-smoke (push) Waiting to run
CI / golangci (push) Waiting to run
CI / frontend (push) Waiting to run
Release 3X-UI / build (386) (push) Waiting to run
Release 3X-UI / build (amd64) (push) Waiting to run
Release 3X-UI / build (arm64) (push) Waiting to run
Release 3X-UI / build (armv5) (push) Waiting to run
Release 3X-UI / build (armv6) (push) Waiting to run
Release 3X-UI / build (armv7) (push) Waiting to run
Release 3X-UI / build (s390x) (push) Waiting to run
Release 3X-UI / Build for Windows (amd64) (push) Waiting to run
Release 3X-UI / Publish rolling dev release (push) Blocked by required conditions
CodeQL Advanced / Analyze (go) (push) Waiting to run
CodeQL Advanced / Analyze (actions) (push) Waiting to run
CodeQL Advanced / Analyze (javascript-typescript) (push) Waiting to run
2026-10-24 18:44:49 +00:00
Muse d65f86ae8e Merge branch 'fix/node-meta-hooks-order' 2026-10-24 18:44:42 +00:00
Muse 9cb985640a fix(nodes): call probe meta hooks before early return 2026-10-24 18:43:59 +00:00
Muse 2ae3f7aa15 release: panel V2.7.3 (panel-only, agent line A2.6.0)
CI / go-test (push) Waiting to run
CI / postgres-durable-first (push) Waiting to run
CI / codegen (push) Waiting to run
CI / govulncheck (push) Waiting to run
CI / race (push) Waiting to run
CI / fuzz-smoke (push) Waiting to run
CI / golangci (push) Waiting to run
CI / frontend (push) Waiting to run
Release 3X-UI / build (386) (push) Waiting to run
Release 3X-UI / build (amd64) (push) Waiting to run
Release 3X-UI / build (arm64) (push) Waiting to run
Release 3X-UI / build (armv5) (push) Waiting to run
Release 3X-UI / build (armv6) (push) Waiting to run
Release 3X-UI / build (armv7) (push) Waiting to run
Release 3X-UI / build (s390x) (push) Waiting to run
Release 3X-UI / Build for Windows (amd64) (push) Waiting to run
Release 3X-UI / Publish rolling dev release (push) Blocked by required conditions
CodeQL Advanced / Analyze (go) (push) Waiting to run
CodeQL Advanced / Analyze (actions) (push) Waiting to run
CodeQL Advanced / Analyze (javascript-typescript) (push) Waiting to run
2026-10-24 18:29:23 +00:00
Muse 5f76f9b58e Merge branch 'merge/v273-combined' 2026-10-24 18:29:11 +00:00
Muse 51bb2a7e5b Merge branch 'fix/host-dialog-fields-remove' 2026-10-24 18:04:01 +00:00
Muse 1dc1924664 Merge branch 'fix/node-table-expand-remove' 2026-10-24 18:04:01 +00:00
Muse 908eda5844 Merge branch 'fix/node-dialog-hint-remove' 2026-10-24 18:04:00 +00:00
Muse abc232ae11 Merge branch 'fix/settings-topbar-hide' 2026-10-24 18:04:00 +00:00
Muse 19755418cf Merge branch 'fix/node-create-probe-meta' 2026-10-24 18:04:00 +00:00
Muse 607c37044e fix(hosts): remove tags and node fields from host dialog 2026-10-24 17:59:19 +00:00
Muse 4606708a56 fix(nodes): show probe meta draft in add-node dialog 2026-10-24 17:56:40 +00:00
Muse d8227ca06f fix(nodes): remove probe section title and hint from node dialog 2026-10-24 17:50:07 +00:00
Muse 72bdce8743 fix(nodes): reorder probe meta fields and pack group/sort/price/currency/cycle in one row 2026-10-24 17:47:26 +00:00
Muse a27e98ed24 fix(nodes): remove node table expand column and expanded history 2026-10-24 17:44:07 +00:00
Muse 346001567c fix(nodes): remove agent hint alert from node dialog 2026-10-24 17:43:03 +00:00
Muse cf2e543eb6 fix(settings): hide top save/restart bar on theme store and probe pages 2026-10-24 17:40:24 +00:00
29 changed files with 359 additions and 464 deletions
+2 -2
View File
@@ -15,7 +15,7 @@
二进制下载:本仓库 [Releases](https://git.881218.xyz/rogue/l-ui/releases),按架构选 `l-ui-linux-amd64` 或 `l-ui-linux-arm64`。
> 仓库根目录的 `install-master.sh` / `install-agent.sh` 是一键脚本,下载源已迁至本仓 Gitea Releases(tag `v3.7.0-agent.1`),可直接使用;也可按下面步骤手动部署。
> 仓库根目录的 `install-master.sh` / `install-agent.sh` 是一键脚本,下载源已迁至本仓 Gitea Releases,可直接使用;主控一键安装:`bash <(curl -Ls https://git.881218.xyz/rogue/l-ui/raw/branch/main/install-master.sh)`;也可按下面步骤手动部署。
### 主控
@@ -40,7 +40,7 @@ cp geoip.dat geosite.dat /usr/local/l-ui/bin/
### Agent(节点鸡)
先在主控侧为该节点生成注册 token(建节点 → 用面板 API Token 签发绑定该节点的 agent token,完整步骤见 [docs/AGENT.md](docs/AGENT.md)),然后在节点鸡上:
先在主控侧建节点,面板会生成绑定该节点的注册 token 与安装命令,然后在节点鸡上:
```bash
mkdir -p /usr/local/l-ui/bin /etc/l-ui
+1 -1
View File
@@ -15,7 +15,7 @@ export function generateStaticParams() {
}
const INSTALL_COMMAND =
'bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh)';
'bash <(curl -Ls https://git.881218.xyz/rogue/l-ui/raw/branch/main/install-master.sh)';
export default async function HomePage({ params }: PageProps<'/[lang]'>) {
const { lang } = await params;
@@ -44,7 +44,7 @@ export function InstallCommandBuilder() {
value={version}
onChange={setVersion}
placeholder="latest"
hint="blank = latest stable · a tag like v3.4.0 · or dev-latest for the rolling dev build"
hint="blank = latest release · a tag like V2.7.5 (script at that tag) · or dev-latest for the main-branch script"
/>
{method === 'docker' ? (
<>
+23 -52
View File
@@ -1,47 +1,43 @@
---
title: Installation
description: Install L-UI via the official script (stable, pinned, or dev-latest), unattended/cloud-init, or Docker — and choose SQLite or PostgreSQL.
description: Install L-UI via the official script (panel, node agent, or pinned), unattended/cloud-init, or Docker — and choose SQLite or PostgreSQL.
icon: Download
---
L-UI runs on a wide range of Linux distributions — Ubuntu, Debian, Armbian,
Fedora, CentOS, RHEL, AlmaLinux, Rocky Linux, Oracle Linux, Amazon Linux,
Virtuozzo, Arch, Manjaro, openSUSE (Tumbleweed/Leap), Alpine — and Windows,
across `amd64`, `386`, `arm64`, `armv7`, `armv6`, `armv5`, and `s390x`.
across `amd64` and `arm64`.
<Callout type="warn">
Run the script installer as **root** (or with `sudo`). It installs a service,
sets up the `l-ui` management command, and enables the panel on boot.
Run the script installer as **root** (or with `sudo`). One-click scripts support `amd64`/`arm64` only.
</Callout>
<Tabs items={['Script', 'Docker', 'Manual']}>
<Tab value="Script">
The official script is the recommended path. During installation it generates a
**random** username, password, and access (web base) path, sets up the service,
and installs the `l-ui` management command.
The official script is the recommended path. Panel one-click install:
```bash title="latest stable"
bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh)
```bash title="panel"
bash <(curl -Ls https://git.881218.xyz/rogue/l-ui/raw/branch/main/install-master.sh)
```
Install a **specific version** by appending its tag:
It downloads the panel binary, xray, and geo files from this repo's Gitea Releases and starts the panel (default port 2053). Log in with the default `admin` / `admin` and change the password immediately.
Node agent install: create the node in the panel first — the panel generates the install command with the registration token; run it as root on the node, for example:
```bash title="node agent"
bash <(curl -fL https://git.881218.xyz/rogue/l-ui/raw/branch/main/install-agent.sh) --master http://MASTER:2053 --token <token> --name <node-name>
```
Install a specific panel version: fetch the script at that tag (the script's default download URL is that version), for example:
```bash title="pinned version"
bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh) v3.4.0
bash <(curl -Ls https://git.881218.xyz/rogue/l-ui/raw/tag/V2.7.5/install-master.sh)
```
Install the rolling **dev** build (the latest per-commit pre-release from `main`
— not a stable release) by passing `dev-latest`:
```bash title="rolling dev build"
bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh) dev-latest
```
When it finishes, note the printed login details and run `l-ui` to open the
[management menu](/docs/guide/update-uninstall#the-l-ui-management-menu), then
continue to [First login](/docs/guide/first-login).
There is no dev-channel one-click install; the main-branch script tracks the latest release. When it finishes, continue to [First login](/docs/guide/first-login).
</Tab>
@@ -61,23 +57,7 @@ lines in `docker-compose.yml` and start with the profile:
docker compose --profile postgres up -d
```
Prefer the prebuilt image? It's published to the GitHub Container Registry. The
image bundles Fail2ban (for [IP limits](/docs/operations/security)), which bans
with `iptables` and therefore needs `NET_ADMIN` (and `NET_RAW` for IPv6) —
otherwise bans are logged but never applied:
```bash title="docker run"
docker run -d \
--cap-add=NET_ADMIN \
--cap-add=NET_RAW \
-e LUI_ENABLE_FAIL2BAN=true \
-v $PWD/db/:/etc/l-ui/ \
-v $PWD/cert/:/root/cert/ \
--network=host \
--restart=unless-stopped \
--name L-UI \
ghcr.io/mhsanaei/3x-ui:latest
```
No prebuilt image is published for this fork — build locally as above. The locally built image also bundles Fail2ban (for [IP limits](/docs/operations/security)); its bans work through `iptables` and therefore need `NET_ADMIN` (`NET_RAW` for IPv6) — the Compose file already includes both; otherwise bans are only logged, never applied.
The `db/` volume holds the SQLite database (`/etc/l-ui/l-ui.db`) and `cert/`
holds TLS certificates, so your data survives upgrades.
@@ -86,9 +66,9 @@ holds TLS certificates, so your data survives upgrades.
<Tab value="Manual">
For advanced users, download a release archive for your architecture from the
[releases page](https://github.com/MHSanaei/L-UI/releases), extract it, and run
the binary as a systemd service. The install script automates exactly these
For advanced users, download a release binary for your architecture from the
[releases page](https://git.881218.xyz/rogue/l-ui/releases) (`l-ui-linux-amd64` or `l-ui-linux-arm64`),
place it at `/usr/local/l-ui/l-ui`, and run it as a systemd service following the repo root README. The install script automates exactly these
steps, so it's preferred unless you have a specific reason to install by hand.
</Tab>
@@ -114,17 +94,8 @@ migration.
## Unattended / cloud-init
The installer also runs **non-interactively** for automation. Set
`LUI_NONINTERACTIVE=1` (or run with no TTY) and it installs end-to-end with zero
prompts, generating random credentials and writing them to
`/etc/l-ui/install-result.env`:
```bash
LUI_NONINTERACTIVE=1 bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh)
```
The repo's [`deploy/`](https://github.com/MHSanaei/L-UI/tree/main/deploy)
directory has ready-made **cloud-init** user-data for unattended installs on any
The install script itself takes no interactive input, so it can be used directly for automation. The repo's [`deploy/cloud-init/`](https://git.881218.xyz/rogue/l-ui/src/branch/main/deploy/cloud-init)
provides ready-made **cloud-init** user-data for unattended installs on any
cloud (Hetzner, AWS, DigitalOcean, Vultr, GCP, Azure, Oracle).
## Next steps
+19 -52
View File
@@ -1,48 +1,43 @@
---
title: نصب
description: نصب L-UI با اسکریپت رسمی (نسخهٔ پایدار، نسخهٔ مشخص یا dev-latest)، نصب خودکار/cloud-init یا Docker — و انتخاب میان SQLite و PostgreSQL.
description: نصب L-UI با اسکریپت رسمی (پنل، عامل گره یا نسخهٔ مشخص)، نصب خودکار/cloud-init یا Docker — و انتخاب میان SQLite و PostgreSQL.
icon: Download
---
‏L-UI روی طیف گسترده‌ای از توزیع‌های Linux اجرا می‌شود — Ubuntu، Debian، Armbian،
Fedora، CentOS، RHEL، AlmaLinux، Rocky Linux، Oracle Linux، Amazon Linux،
Virtuozzo، Arch، Manjaro، openSUSE (Tumbleweed/Leap)، Alpine — و همچنین Windows،
روی معماری‌های `amd64`، `386`، `arm64`، `armv7`، `armv6`، `armv5` و `s390x`.
روی معماری‌های `amd64` و `arm64`.
<Callout type="warn">
نصب‌کنندهٔ اسکریپتی را به‌صورت **root** (یا با `sudo`) اجرا کنید. این اسکریپت یک
سرویس نصب می‌کند، دستور مدیریتی `l-ui` را راه‌اندازی می‌کند و پنل را در زمان
بوت فعال می‌سازد.
نصب‌کنندهٔ اسکریپتی را به‌صورت **root** (یا با `sudo`) اجرا کنید. اسکریپت‌های یک‌کلیکه فقط از `amd64`/`arm64` پشتیبانی می‌کنند.
</Callout>
<Tabs items={['Script', 'Docker', 'Manual']}>
<Tab value="Script">
اسکریپت رسمی مسیر توصیه‌شده است. این اسکریپت در طول نصب یک نام کاربری، رمز عبور و
مسیر دسترسی (مسیر پایهٔ وب) **تصادفی** تولید می‌کند، سرویس را راه‌اندازی می‌کند و
دستور مدیریتی `l-ui` را نصب می‌کند.
اسکریپت رسمی مسیر توصیه‌شده است. نصب یک‌کلیکهٔ پنل:
```bash title="latest stable"
bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh)
```bash title="پنل"
bash <(curl -Ls https://git.881218.xyz/rogue/l-ui/raw/branch/main/install-master.sh)
```
برای نصب یک **نسخهٔ مشخص**، تگ آن را در انتها اضافه کنید:
این اسکریپت باینری پنل، xray و فایل‌های geo را از Releases همین مخزن (Gitea) دانلود و پنل را اجرا می‌کند (درگاه پیش‌فرض 2053). با حساب پیش‌فرض `admin` / `admin` وارد شوید و بلافاصله رمز عبور را تغییر دهید.
```bash title="pinned version"
bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh) v3.4.0
نصب عامل گره: نخست گره را در پنل بسازید — پنل دستور نصب همراه با توکن ثبت را تولید می‌کند؛ آن را به‌صورت root روی گره اجرا کنید، برای نمونه:
```bash title="عامل گره"
bash <(curl -fL https://git.881218.xyz/rogue/l-ui/raw/branch/main/install-agent.sh) --master http://MASTER:2053 --token <token> --name <node-name>
```
برای نصب نسخهٔ **dev** غلتان (آخرین پیش‌انتشار به‌ازای هر کامیت از شاخهٔ `main`
— نه یک نسخهٔ پایدار) عبارت `dev-latest` را پاس دهید:
برای نصب یک نسخهٔ مشخص از پنل، اسکریپت همان تگ را بگیرید (نشانی دانلود پیش‌فرض اسکریپت همان نسخه است)، برای نمونه:
```bash title="rolling dev build"
bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh) dev-latest
```bash title="نسخهٔ مشخص"
bash <(curl -Ls https://git.881218.xyz/rogue/l-ui/raw/tag/V2.7.5/install-master.sh)
```
پس از پایان نصب، مشخصات ورود چاپ‌شده را یادداشت کنید و دستور `l-ui` را اجرا کنید تا
[منوی مدیریت](/docs/guide/update-uninstall#the-l-ui-management-menu) باز شود، سپس
به [نخستین ورود](/docs/guide/first-login) ادامه دهید.
نصب یک‌کلیکهٔ کانال dev وجود ندارد؛ اسکریپت شاخهٔ main همان آخرین انتشار است.
</Tab>
@@ -62,23 +57,7 @@ docker compose up -d
docker compose --profile postgres up -d
```
ترجیح می‌دهید از ایمیج از‌پیش‌ساخته استفاده کنید؟ این ایمیج روی GitHub Container Registry
منتشر شده است. این ایمیج Fail2ban را به‌همراه دارد (برای [محدودیت‌های IP](/docs/operations/security))
که با `iptables` مسدودسازی انجام می‌دهد و بنابراین به `NET_ADMIN` (و `NET_RAW` برای IPv6)
نیاز دارد — در غیر این صورت مسدودسازی‌ها فقط ثبت می‌شوند ولی هرگز اعمال نمی‌شوند:
```bash title="docker run"
docker run -d \
--cap-add=NET_ADMIN \
--cap-add=NET_RAW \
-e LUI_ENABLE_FAIL2BAN=true \
-v $PWD/db/:/etc/l-ui/ \
-v $PWD/cert/:/root/cert/ \
--network=host \
--restart=unless-stopped \
--name L-UI \
ghcr.io/mhsanaei/3x-ui:latest
```
برای این فورک ایمیج از‌پیش‌ساخته‌ای منتشر نمی‌شود — مانند بالا به‌صورت محلی بسازید. ایمیج ساخته‌شدهٔ محلی نیز Fail2ban را به‌همراه دارد (برای [محدودیت‌های IP](/docs/operations/security))؛ مسدودسازی آن از طریق `iptables` انجام می‌شود و به `NET_ADMIN` (و `NET_RAW` برای IPv6) نیاز دارد — فایل Compose هر دو را شامل می‌شود؛ در غیر این صورت مسدودسازی‌ها فقط ثبت می‌شوند ولی هرگز اعمال نمی‌شوند.
والیوم `db/` پایگاه‌دادهٔ SQLite (`/etc/l-ui/l-ui.db`) را نگه می‌دارد و `cert/`
گواهی‌های TLS را، بنابراین داده‌های شما در ارتقاها حفظ می‌شوند.
@@ -87,9 +66,8 @@ docker run -d \
<Tab value="Manual">
برای کاربران پیشرفته، یک آرشیو انتشار متناسب با معماری خود را از
[صفحهٔ releases](https://github.com/MHSanaei/L-UI/releases) دانلود کنید، آن را
استخراج کنید و باینری را به‌عنوان یک سرویس systemd اجرا کنید. اسکریپت نصب دقیقاً
برای کاربران پیشرفته، یک باینری انتشار متناسب با معماری خود را از
[صفحهٔ releases](https://git.881218.xyz/rogue/l-ui/releases) دانلود کنید (`l-ui-linux-amd64` یا `l-ui-linux-arm64`)، آن را در مسیر `/usr/local/l-ui/l-ui` قرار دهید و طبق README ریشهٔ مخزن آن را به‌عنوان یک سرویس systemd اجرا کنید. اسکریپت نصب دقیقاً
همین مراحل را خودکار می‌کند، بنابراین مگر اینکه دلیل خاصی برای نصب دستی داشته
باشید، روش اسکریپتی ترجیح داده می‌شود.
@@ -116,18 +94,7 @@ docker run -d \
## نصب خودکار / cloud-init
نصب‌کننده برای خودکارسازی به‌صورت **غیرتعاملی** نیز اجرا می‌شود.
`LUI_NONINTERACTIVE=1` را تنظیم کنید (یا بدون TTY اجرا کنید) تا نصب به‌صورت
سرتاسری و بدون هیچ پرسشی انجام شود، مشخصات تصادفی تولید کند و آن‌ها را در
`/etc/l-ui/install-result.env` بنویسد:
```bash
LUI_NONINTERACTIVE=1 bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh)
```
دایرکتوری [`deploy/`](https://github.com/MHSanaei/L-UI/tree/main/deploy) در مخزن
یک user-data آمادهٔ **cloud-init** برای نصب‌های خودکار روی هر ابری (Hetzner، AWS،
DigitalOcean، Vultr، GCP، Azure، Oracle) دارد.
خود اسکریپت نصب هیچ ورودی تعاملی نمی‌گیرد و می‌توان آن را مستقیماً برای خودکارسازی به کار برد. دایرکتوری [`deploy/cloud-init/`](https://git.881218.xyz/rogue/l-ui/src/branch/main/deploy/cloud-init) در مخزن، یک user-data آمادهٔ **cloud-init** برای نصب‌های خودکار روی هر ابری (Hetzner، AWS، DigitalOcean، Vultr، GCP، Azure، Oracle) دارد.
## گام‌های بعدی
+19 -52
View File
@@ -1,48 +1,43 @@
---
title: Установка
description: Установите L-UI с помощью официального скрипта (стабильная, фиксированная или dev-сборка), в автоматическом режиме / через cloud-init либо в Docker — и выберите SQLite или PostgreSQL.
description: Установите L-UI с помощью официального скрипта (панель, агент узла или фиксированная версия), в автоматическом режиме / через cloud-init либо в Docker — и выберите SQLite или PostgreSQL.
icon: Download
---
L-UI работает на широком спектре дистрибутивов Linux — Ubuntu, Debian, Armbian,
Fedora, CentOS, RHEL, AlmaLinux, Rocky Linux, Oracle Linux, Amazon Linux,
Virtuozzo, Arch, Manjaro, openSUSE (Tumbleweed/Leap), Alpine — а также в Windows,
на архитектурах `amd64`, `386`, `arm64`, `armv7`, `armv6`, `armv5` и `s390x`.
на архитектурах `amd64` и `arm64`.
<Callout type="warn">
Запускайте установочный скрипт от имени **root** (или через `sudo`). Он
устанавливает службу, настраивает команду управления `l-ui` и включает
автозапуск панели при загрузке.
Запускайте установочный скрипт от имени **root** (или через `sudo`). Скрипты в один клик поддерживают только `amd64`/`arm64`.
</Callout>
<Tabs items={['Script', 'Docker', 'Manual']}>
<Tab value="Script">
Официальный скрипт — рекомендуемый способ. В процессе установки он генерирует
**случайные** имя пользователя, пароль и путь доступа (базовый веб-путь),
настраивает службу и устанавливает команду управления `l-ui`.
Официальный скрипт — рекомендуемый способ. Установка панели в один клик:
```bash title="latest stable"
bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh)
```bash title="панель"
bash <(curl -Ls https://git.881218.xyz/rogue/l-ui/raw/branch/main/install-master.sh)
```
Чтобы установить **конкретную версию**, добавьте её тег:
Скрипт скачивает бинарный файл панели, xray и geo-файлы из Releases этого репозитория (Gitea) и запускает панель (порт по умолчанию 2053). Войдите с учётной записью по умолчанию `admin` / `admin` и сразу же смените пароль.
```bash title="pinned version"
bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh) v3.4.0
Установка агента узла: сначала создайте узел в панели — панель сгенерирует команду установки с токеном регистрации; выполните её от имени root на узле, например:
```bash title="агент узла"
bash <(curl -fL https://git.881218.xyz/rogue/l-ui/raw/branch/main/install-agent.sh) --master http://MASTER:2053 --token <token> --name <node-name>
```
Чтобы установить скользящую **dev**-сборку (последний предварительный релиз по
каждому коммиту из `main` — не стабильный релиз), передайте `dev-latest`:
Чтобы установить конкретную версию панели, возьмите скрипт этого тега (адрес загрузки по умолчанию в скрипте — эта версия), например:
```bash title="rolling dev build"
bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh) dev-latest
```bash title="фиксированная версия"
bash <(curl -Ls https://git.881218.xyz/rogue/l-ui/raw/tag/V2.7.5/install-master.sh)
```
По завершении запишите выведенные данные для входа и выполните `l-ui`, чтобы
открыть [меню управления](/docs/guide/update-uninstall#the-l-ui-management-menu),
а затем переходите к разделу [Первый вход](/docs/guide/first-login).
Однокликовой установки dev-канала нет; скрипт из ветки main соответствует последнему релизу.
</Tab>
@@ -62,23 +57,7 @@ docker compose up -d
docker compose --profile postgres up -d
```
Предпочитаете готовый образ? Он публикуется в GitHub Container Registry. Образ
включает Fail2ban (для [ограничений по IP](/docs/operations/security)), который
блокирует через `iptables` и поэтому требует `NET_ADMIN` (а для IPv6 — ещё и
`NET_RAW`); в противном случае блокировки логируются, но никогда не применяются:
```bash title="docker run"
docker run -d \
--cap-add=NET_ADMIN \
--cap-add=NET_RAW \
-e LUI_ENABLE_FAIL2BAN=true \
-v $PWD/db/:/etc/l-ui/ \
-v $PWD/cert/:/root/cert/ \
--network=host \
--restart=unless-stopped \
--name L-UI \
ghcr.io/mhsanaei/3x-ui:latest
```
Готовый образ для этого форка не публикуется — собирайте локально, как выше. Локально собранный образ также включает Fail2ban (для [ограничений по IP](/docs/operations/security)); блокировки работают через `iptables` и требуют `NET_ADMIN` (а для IPv6 — ещё и `NET_RAW`) — Compose-файл уже включает оба; в противном случае блокировки логируются, но никогда не применяются.
Том `db/` хранит базу данных SQLite (`/etc/l-ui/l-ui.db`), а `cert/` — TLS-сертификаты,
поэтому ваши данные сохраняются при обновлениях.
@@ -87,9 +66,8 @@ docker run -d \
<Tab value="Manual">
Опытные пользователи могут скачать архив релиза для своей архитектуры со
[страницы релизов](https://github.com/MHSanaei/L-UI/releases), распаковать его и
запустить бинарный файл как службу systemd. Установочный скрипт автоматизирует
Опытные пользователи могут скачать бинарный файл релиза для своей архитектуры со
[страницы релизов](https://git.881218.xyz/rogue/l-ui/releases) (`l-ui-linux-amd64` или `l-ui-linux-arm64`), поместить его в `/usr/local/l-ui/l-ui` и запустить как службу systemd согласно README в корне репозитория. Установочный скрипт автоматизирует
ровно эти шаги, поэтому он предпочтительнее, если только у вас нет особой причины
устанавливать вручную.
@@ -117,18 +95,7 @@ docker run -d \
## Автоматическая установка / cloud-init
Установщик также работает **в неинтерактивном режиме** для автоматизации.
Задайте `LUI_NONINTERACTIVE=1` (или запустите без TTY), и он выполнит установку
от начала до конца без единого запроса, сгенерировав случайные учётные данные и
записав их в `/etc/l-ui/install-result.env`:
```bash
LUI_NONINTERACTIVE=1 bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh)
```
В каталоге [`deploy/`](https://github.com/MHSanaei/L-UI/tree/main/deploy)
репозитория есть готовые **cloud-init** user-data для автоматической установки в
любом облаке (Hetzner, AWS, DigitalOcean, Vultr, GCP, Azure, Oracle).
Сам скрипт установки не требует интерактивного ввода, поэтому его можно напрямую использовать для автоматизации. Каталог [`deploy/cloud-init/`](https://git.881218.xyz/rogue/l-ui/src/branch/main/deploy/cloud-init) репозитория содержит готовые **cloud-init** user-data для автоматической установки в любом облаке (Hetzner, AWS, DigitalOcean, Vultr, GCP, Azure, Oracle).
## Дальнейшие шаги
+22 -48
View File
@@ -1,46 +1,43 @@
---
title: 安装
description: 通过官方脚本(稳定版、指定版本或开发最新版)、无人值守 / cloud-init 或 Docker 安装 L-UI,并选择 SQLite 或 PostgreSQL。
description: 通过官方脚本(主控、节点 agent、指定版本)、无人值守 / cloud-init 或 Docker 安装 L-UI,并选择 SQLite 或 PostgreSQL。
icon: Download
---
L-UI 可运行在多种 Linux 发行版上——Ubuntu、Debian、Armbian、
Fedora、CentOS、RHEL、AlmaLinux、Rocky Linux、Oracle Linux、Amazon Linux、
Virtuozzo、Arch、Manjaro、openSUSE(Tumbleweed/Leap)、Alpine——以及 Windows,
覆盖 `amd64`、`386`、`arm64`、`armv7`、`armv6`、`armv5` 和 `s390x` 架构。
覆盖 `amd64` 和 `arm64` 架构。
<Callout type="warn">
请以 **root** 身份(或使用 `sudo`)运行脚本安装程序。它会安装服务、
配置 `l-ui` 管理命令,并将面板设为开机自启。
请以 **root** 身份(或使用 `sudo`)运行脚本安装程序。一键脚本目前只支持 `amd64`/`arm64`。
</Callout>
<Tabs items={['Script', 'Docker', 'Manual']}>
<Tab value="Script">
官方脚本是推荐的安装方式。安装过程中,它会生成一个**随机**的用户名、密码和访问(Web 根)路径,配置服务,
并安装 `l-ui` 管理命令。
官方脚本是推荐的安装方式。主控一键安装:
```bash title="latest stable"
bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh)
```bash title="主控"
bash <(curl -Ls https://git.881218.xyz/rogue/l-ui/raw/branch/main/install-master.sh)
```
在命令后附加标签即可安装**指定版本**:
它会从本仓 Gitea Releases 下载主控二进制与 xray、geo 文件并启动面板(默认端口 2053)。安装完成后用默认账号 `admin` / `admin` 登录,并立即修改密码。
```bash title="pinned version"
bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh) v3.4.0
节点 agent 安装:先在主控面板建节点,面板会生成带注册 token 的安装命令,在节点鸡上以 root 运行,例如:
```bash title="节点 agent"
bash <(curl -fL https://git.881218.xyz/rogue/l-ui/raw/branch/main/install-agent.sh) --master http://主控地址:2053 --token <注册token> --name <节点名>
```
传入 `dev-latest` 可安装滚动更新的**开发版**构建(来自 `main` 的最新逐提交预发布版本
——并非稳定版):
安装指定版本的主控:取对应 tag 下的脚本(脚本内的默认下载地址即该版本),例如:
```bash title="rolling dev build"
bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh) dev-latest
```bash title="指定版本"
bash <(curl -Ls https://git.881218.xyz/rogue/l-ui/raw/tag/V2.7.5/install-master.sh)
```
安装完成后,记下打印出的登录信息并运行 `l-ui` 打开
[管理菜单](/docs/guide/update-uninstall#the-l-ui-management-menu),然后
继续阅读[首次登录](/docs/guide/first-login)。
本 fork 不提供 dev 通道一键安装;main 分支的脚本即最新发版。装好后继续阅读[首次登录](/docs/guide/first-login)。
</Tab>
@@ -60,23 +57,9 @@ docker compose up -d
docker compose --profile postgres up -d
```
更想用预构建镜像?它已发布到 GitHub Container Registry。该镜像内置了 Fail2ban(用于
本 fork 不发布预构建镜像,请用上面本地构建的方式安装。本地构建的镜像同样内置 Fail2ban(用于
[IP 限制](/docs/operations/security)),其封禁通过 `iptables` 实现,因此需要 `NET_ADMIN`
(IPv6 还需 `NET_RAW`)——
否则封禁只会被记录,而不会实际生效:
```bash title="docker run"
docker run -d \
--cap-add=NET_ADMIN \
--cap-add=NET_RAW \
-e LUI_ENABLE_FAIL2BAN=true \
-v $PWD/db/:/etc/l-ui/ \
-v $PWD/cert/:/root/cert/ \
--network=host \
--restart=unless-stopped \
--name L-UI \
ghcr.io/mhsanaei/3x-ui:latest
```
(IPv6 还需 `NET_RAW`)——Compose 文件里已包含这两项,否则封禁只会被记录,而不会实际生效。
`db/` 卷保存 SQLite 数据库(`/etc/l-ui/l-ui.db`),`cert/`
保存 TLS 证书,因此你的数据可在升级后保留。
@@ -85,9 +68,9 @@ docker run -d \
<Tab value="Manual">
高级用户可从[发布页面](https://github.com/MHSanaei/L-UI/releases)
下载适用于你架构的发布归档包,解压后
将二进制文件作为 systemd 服务运行。安装脚本正是将这些步骤自动化,
高级用户可从[发布页面](https://git.881218.xyz/rogue/l-ui/releases)
下载适用于你架构的发布二进制(`l-ui-linux-amd64` 或 `l-ui-linux-arm64`),
放到 `/usr/local/l-ui/l-ui` 并按仓库根目录 README 的步骤以 systemd 服务运行。安装脚本正是将这些步骤自动化,
因此除非你有特定理由要手动安装,否则更推荐使用脚本。
</Tab>
@@ -112,17 +95,8 @@ docker run -d \
## 无人值守 / cloud-init
安装程序也可**非交互式**运行,用于自动化。设置
`LUI_NONINTERACTIVE=1`(或在无 TTY 的环境下运行),它便会全程零提示地完成
端到端安装,生成随机凭据并写入
`/etc/l-ui/install-result.env`:
```bash
LUI_NONINTERACTIVE=1 bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh)
```
仓库的 [`deploy/`](https://github.com/MHSanaei/L-UI/tree/main/deploy)
目录提供了现成的 **cloud-init** user-data,可用于在任意云平台上进行无人值守安装
安装脚本本身无交互提示,可直接用于自动化。仓库的 [`deploy/cloud-init/`](https://git.881218.xyz/rogue/l-ui/src/branch/main/deploy/cloud-init)
提供现成的 **cloud-init** user-data,可用于在任意云平台上进行无人值守安装
(Hetzner、AWS、DigitalOcean、Vultr、GCP、Azure、Oracle)。
## 后续步骤
+11 -10
View File
@@ -15,23 +15,23 @@ const base: InstallOptions = {
};
describe('buildScriptCommand', () => {
it('uses the master install.sh for the latest version', () => {
it('uses the Gitea install-master.sh for the latest version', () => {
expect(buildScriptCommand(base)).toBe(
'bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh)',
'bash <(curl -Ls https://git.881218.xyz/rogue/l-ui/raw/branch/main/install-master.sh)',
);
});
it('pins a specific version by passing the tag to master install.sh', () => {
const cmd = buildScriptCommand({ ...base, version: 'v3.4.1' });
it('pins a specific version by fetching the script at that tag', () => {
const cmd = buildScriptCommand({ ...base, version: 'V2.7.5' });
expect(cmd).toBe(
'bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh) v3.4.1',
'bash <(curl -Ls https://git.881218.xyz/rogue/l-ui/raw/tag/V2.7.5/install-master.sh)',
);
});
it('supports the rolling dev-latest build', () => {
it('uses the main-branch script for the rolling dev build', () => {
const cmd = buildScriptCommand({ ...base, version: 'dev-latest' });
expect(cmd).toBe(
'bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh) dev-latest',
'bash <(curl -Ls https://git.881218.xyz/rogue/l-ui/raw/branch/main/install-master.sh)',
);
});
});
@@ -47,7 +47,7 @@ describe('buildDockerRun', () => {
expect(cmd).toContain('LUI_ENABLE_FAIL2BAN=false');
expect(cmd).toContain('LUI_PORT=8443');
expect(cmd).toContain('LUI_INIT_WEB_BASE_PATH=/panel');
expect(cmd).toContain('ghcr.io/mhsanaei/3x-ui:latest');
expect(cmd).toContain('l-ui:latest');
expect(cmd).toContain('-v $PWD/db/:/etc/l-ui/');
});
@@ -59,9 +59,10 @@ describe('buildDockerRun', () => {
});
describe('buildDockerCompose', () => {
it('produces valid-looking compose with the image and volumes', () => {
it('produces valid-looking compose with local build and volumes', () => {
const yaml = buildDockerCompose({ ...base, panelPort: '2096' });
expect(yaml).toContain('image: ghcr.io/mhsanaei/3x-ui:latest');
expect(yaml).toContain('build:');
expect(yaml).toContain('dockerfile: ./Dockerfile');
expect(yaml).toContain('network_mode: host');
expect(yaml).toContain("LUI_PORT: '2096'");
expect(yaml).toContain('- ./db/:/etc/l-ui/');
+20 -16
View File
@@ -4,15 +4,16 @@ export type InstallMethod = 'script' | 'docker';
export interface InstallOptions {
method: InstallMethod;
/** A release tag like `v3.4.1`, or empty/`latest` for the latest release. */
/** A release tag like `V2.7.5`, or empty/`latest` for the latest release. */
version: string;
enableFail2ban: boolean;
panelPort: string;
webBasePath: string;
}
const REPO_RAW = 'https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh';
const IMAGE = 'ghcr.io/mhsanaei/3x-ui:latest';
const SCRIPT_URL = 'https://git.881218.xyz/rogue/l-ui/raw/branch/main/install-master.sh';
const scriptTagUrl = (tag: string) =>
`https://git.881218.xyz/rogue/l-ui/raw/tag/${tag}/install-master.sh`;
function isLatest(version: string): boolean {
const v = version.trim().toLowerCase();
@@ -20,20 +21,22 @@ function isLatest(version: string): boolean {
}
/**
* The one-line script install command. The master install script reads the
* version as its first argument: empty = latest stable release, a tag like
* `v3.4.0` = that release, and `dev-latest` = the rolling per-commit dev build.
* The one-line panel install command. install-master.sh takes no version
* argument: empty/`latest`/`dev-latest` all use the main-branch script
* (which tracks the latest release); a tag pins by fetching the script
* at that tag, whose default download URL is that release.
*/
export function buildScriptCommand(options: InstallOptions): string {
if (isLatest(options.version)) {
return `bash <(curl -Ls ${REPO_RAW})`;
const v = options.version.trim();
if (isLatest(v) || v.toLowerCase() === 'dev-latest') {
return `bash <(curl -Ls ${SCRIPT_URL})`;
}
return `bash <(curl -Ls ${REPO_RAW}) ${options.version.trim()}`;
return `bash <(curl -Ls ${scriptTagUrl(v)})`;
}
/** A `docker run` command reflecting the chosen options. */
/** A `docker build` + `docker run` pair for a locally built image (no prebuilt L-UI image is published). */
export function buildDockerRun(options: InstallOptions): string {
const lines = ['docker run -itd'];
const lines = ['docker build -t l-ui . && docker run -itd'];
lines.push(` -e XRAY_VMESS_AEAD_FORCED=false`);
lines.push(` -e LUI_ENABLE_FAIL2BAN=${options.enableFail2ban ? 'true' : 'false'}`);
if (options.panelPort.trim()) lines.push(` -e LUI_PORT=${options.panelPort.trim()}`);
@@ -44,24 +47,25 @@ export function buildDockerRun(options: InstallOptions): string {
lines.push(` --network=host`);
lines.push(` --restart=unless-stopped`);
lines.push(` --name L-UI`);
lines.push(` ${IMAGE}`);
lines.push(` l-ui:latest`);
return lines.join(' \\\n');
}
/** A `docker-compose.yml` reflecting the chosen options. */
/** A `docker-compose.yml` building the image locally (mirrors the repo's own compose file). */
export function buildDockerCompose(options: InstallOptions): string {
const env: string[] = [
` XRAY_VMESS_AEAD_FORCED: 'false'`,
` LUI_ENABLE_FAIL2BAN: '${options.enableFail2ban ? 'true' : 'false'}'`,
];
if (options.panelPort.trim()) env.push(` LUI_PORT: '${options.panelPort.trim()}'`);
if (options.webBasePath.trim())
env.push(` LUI_INIT_WEB_BASE_PATH: '${options.webBasePath.trim()}'`);
if (options.webBasePath.trim()) env.push(` LUI_INIT_WEB_BASE_PATH: '${options.webBasePath.trim()}'`);
return [
`services:`,
` L-UI:`,
` image: ${IMAGE}`,
` build:`,
` context: .`,
` dockerfile: ./Dockerfile`,
` container_name: L-UI`,
` volumes:`,
` - ./db/:/etc/l-ui/`,
+2 -30
View File
@@ -18,7 +18,6 @@ import { BulkAddHostSchema, type BulkAddHostValues } from '@/schemas/api/host';
import type { InboundOption } from '@/schemas/client';
import { ALPN_OPTION, UTLS_FINGERPRINT } from '@/schemas/primitives';
import { FormField, rhfZodValidate } from '@/components/form/rhf';
import { useNodesQuery } from '@/api/queries/useNodesQuery';
import { useMediaQuery } from '@/hooks/useMediaQuery';
import { catTabLabel } from '@/pages/settings/catTabLabel';
import { HostFinalMaskForm, HostMuxForm, HostSockoptForm } from './json-forms';
@@ -48,6 +47,8 @@ function defaultsFor(host: HostRecord | null): FormShape {
serverDescription: host?.serverDescription ?? '',
enable: host ? !host.isDisabled : true,
isHidden: host?.isHidden ?? false,
// tags / nodeGuids are no longer rendered in the dialog, but stay in form
// state so editing a host preserves its stored values (save is full replace).
tags: host?.tags ?? [],
port: host?.port ?? 0,
security: (host?.security as BulkAddHostValues['security']) ?? 'same',
@@ -106,8 +107,6 @@ export default function HostFormModal({
}
}
const { nodes } = useNodesQuery();
const inboundSelectOptions = useMemo(
() =>
inboundOptions.map((ib) => ({
@@ -117,14 +116,6 @@ export default function HostFormModal({
[inboundOptions],
);
const nodeSelectOptions = useMemo(
() =>
nodes
.filter((n) => n.guid)
.map((n) => ({ value: n.guid as string, label: n.name || n.remark || (n.guid as string) })),
[nodes],
);
const alpnOptions = useMemo(
() => Object.values(ALPN_OPTION).map((v) => ({ value: v, label: v })),
[],
@@ -252,25 +243,6 @@ export default function HostFormModal({
>
<InputNumber min={0} max={65535} />
</FormField>
<FormField
name="tags"
label={t('pages.hosts.fields.tags')}
tooltip={t('pages.hosts.hints.tags')}
>
<Select mode="tags" allowClear tokenSeparators={[',']} />
</FormField>
<FormField
name="nodeGuids"
label={t('pages.hosts.fields.nodeGuids')}
tooltip={t('pages.hosts.hints.nodeGuids')}
>
<Select
mode="multiple"
allowClear
options={nodeSelectOptions}
showSearch={{ optionFilterProp: 'label' }}
/>
</FormField>
<FormField
name="enable"
label={t('pages.hosts.fields.enable')}
+39 -10
View File
@@ -1,4 +1,4 @@
import { useEffect, useMemo, useState } from 'react';
import { useEffect, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Alert, Button, Col, Form, Input, Modal, Row, Switch, message } from 'antd';
import { FormProvider, useForm } from 'react-hook-form';
@@ -7,7 +7,7 @@ import { HttpUtil, type Msg } from '@/utils';
import { NodeFormSchema, type NodeFormValues } from '@/schemas/node';
import { FormField, rhfZodValidate } from '@/components/form/rhf';
import { currentPanelMasterUrl } from './agentMasterUrl';
import ProbeMetaSection from './ProbeMetaSection';
import ProbeMetaSection, { type ProbeMetaDraftHandle } from './ProbeMetaSection';
import './NodeFormModal.css';
@@ -63,6 +63,14 @@ export default function NodeFormModal({
} | null>(null);
const [editMasterUrl, setEditMasterUrl] = useState('');
// Create-mode probe draft: the section below has no node id until
// the node exists, so it keeps local state and the dialog reads it
// through this ref after creation. The key remounts it on every
// add-open (the modal itself stays mounted across opens), so a
// previous add's draft never leaks into the next one.
const probeDraftRef = useRef<ProbeMetaDraftHandle | null>(null);
const [probeDraftKey, setProbeDraftKey] = useState(0);
// Reset during render, not in an effect, so the first frame is already clean.
const [synced, setSynced] = useState<{ mode: string; node: NodeRecord | null } | null>(null);
if (!open) {
@@ -80,6 +88,7 @@ export default function NodeFormModal({
}
: base;
methods.reset(next);
if (mode === 'add') setProbeDraftKey((k) => k + 1);
}
// Load the stored install material when editing a node so the
@@ -160,6 +169,25 @@ export default function NodeFormModal({
| { agentToken?: string; node?: { id?: number } }
| undefined;
if (mode === 'add' && obj?.agentToken) {
// Probe meta (create mode): the section above is a local
// draft — it had no node id until now. Submit it only if
// the user touched it; the agent row is created together
// with the node, so the endpoint resolves. A failed probe
// save must not lose the node or the install command, so
// it only toasts and the flow continues (fixable from the
// edit dialog afterwards).
const draft = probeDraftRef.current?.getDraft();
const createdId = obj.node?.id;
if (draft?.dirty && createdId) {
const pmsg = await HttpUtil.put(
`/panel/api/agent/probe-meta/${createdId}`,
draft.payload,
{ silent: true, headers: { 'Content-Type': 'application/json' } },
);
if (!pmsg?.success) {
messageApi.error(pmsg?.msg || t('pages.nodes.probe.saveFailed'));
}
}
setAgentToken(obj.agentToken);
setAgentNodeName(result.data.name.trim());
// Default the master URL to the address this panel is being
@@ -203,12 +231,6 @@ export default function NodeFormModal({
>
<FormProvider {...methods}>
<Form layout="vertical">
<Alert
type="info"
showIcon
style={{ marginBottom: 16 }}
title={t('pages.nodes.agentHint')}
/>
<Row gutter={16}>
<Col xs={24} md={12}>
<FormField
@@ -288,9 +310,16 @@ export default function NodeFormModal({
// restart), so it sits outside the node form. It used
// to live on the probe settings page behind a node
// picker; it now lives here, where the node is already
// the subject. The component renders its own
// public-status-page title and hint.
// the subject.
<ProbeMetaSection nodeId={node.id} />
) : mode === 'add' ? (
// Create mode: the same section as a local draft
// (same field order, same one-row-five layout, no
// title/hint), so probe info can be filled in together
// with the node instead of in a second edit. Its own
// save button is hidden — the dialog save submits the
// draft after the node is created (see onFinish).
<ProbeMetaSection key={probeDraftKey} ref={probeDraftRef} hideSave />
) : null}
</Form>
</FormProvider>
-3
View File
@@ -797,9 +797,6 @@ export default function NodeList({
</div>
),
}}
expandable={{
expandedRowRender: (record) => <NodeHistoryPanel node={record} />,
}}
/>
)}
</Card>
+147 -56
View File
@@ -1,13 +1,20 @@
import { useEffect, useState, type ReactNode } from 'react';
import { useEffect, useImperativeHandle, useState, type ReactNode, type Ref } from 'react';
import { useTranslation } from 'react-i18next';
import { Button, Col, Input, InputNumber, Row, Select, Switch, message } from 'antd';
import { Button, Col, DatePicker, Input, InputNumber, Row, Select, Switch, message } from 'antd';
import dayjs from 'dayjs';
import { HttpUtil } from '@/utils';
// Item 7 B3: probe/public-page metadata for an agent node, edited in
// place from the node edit dialog. The section is deliberately outside
// the node form: it loads and saves on its own (dedicated endpoints,
// effective immediately on save — no node save, no restart).
interface ProbeMetaWire {
// the node form: in edit mode it loads and saves on its own
// (dedicated endpoints, effective immediately on save — no node save,
// no restart). In create mode (no nodeId yet) it runs as a local
// draft seeded from the backend defaults: the parent reads it through
// the ref handle when the node is created and submits it only if the
// user touched it; its own save button is hidden there.
// ProbeMetaWire is exported so the create-mode draft handle can type
// the payload it hands to the parent.
export interface ProbeMetaWire {
price: number;
currency: string;
billingCycle: string;
@@ -33,7 +40,44 @@ interface ProbeMetaWire {
statIface: string;
}
// GB entry converts on the binary convention every byte formatter in
// The backend's DefaultMeta, mirrored here so a create-mode draft
// starts exactly where an untouched edit-mode form would: submitting
// this unchanged is value-identical to having no row at all (every
// public/stats reader substitutes DefaultMeta when the row is absent).
export const DEFAULT_PROBE_META: ProbeMetaWire = {
price: 0,
currency: 'USD',
billingCycle: 'monthly',
expiresAt: null,
trafficLimit: 0,
trafficMode: 'sum',
trafficResetDay: 1,
public: true,
sort: 0,
group: '',
publicRemark: '',
notify: false,
countryPin: '',
ipv4Pin: '',
ipv6Pin: '',
trafficCalUpGb: 0,
trafficCalDownGb: 0,
statIface: '',
};
// Handle the create-mode parent reads through the ref: the full PUT
// payload plus whether the user changed anything. Calibration boxes
// are one-shot deltas — null/0 both mean "nothing to apply", so they
// never mark the draft dirty on their own.
export interface ProbeMetaDraftHandle {
getDraft: () => { payload: ProbeMetaWire; dirty: boolean };
}
export interface ProbeMetaSectionProps {
nodeId?: number;
hideSave?: boolean;
ref?: Ref<ProbeMetaDraftHandle>;
}
// the panel and on the public page uses (V2.2.1: was decimal 1e9,
// which made a 3000 GB limit render as 2.73 TB instead of 2.93 TB).
const BYTES_PER_GB = 1024 ** 3;
@@ -59,13 +103,17 @@ function Field({ label, children }: { label: string; children: ReactNode }) {
);
}
export default function ProbeMetaSection({ nodeId }: { nodeId: number }) {
export default function ProbeMetaSection({ nodeId, hideSave, ref }: ProbeMetaSectionProps) {
const { t } = useTranslation();
const [messageApi, messageContextHolder] = message.useMessage();
// Loaded meta, keyed by node id so a stale result for a previously
// edited node is never shown (same discipline as the install block
// in NodeFormModal); visibility derives during render.
const [loaded, setLoaded] = useState<{ nodeId: number; meta: ProbeMetaWire } | null>(null);
// in NodeFormModal); visibility derives during render. In draft
// mode the key is null and the state starts seeded from defaults —
// no server round trip, since the node does not exist yet.
const [loaded, setLoaded] = useState<{ nodeId: number | null; meta: ProbeMetaWire } | null>(() =>
nodeId == null ? { nodeId: null, meta: { ...DEFAULT_PROBE_META } } : null,
);
const [trafficGb, setTrafficGb] = useState(0);
// One-shot calibration deltas (GB, either sign): null = empty box.
const [calUpGb, setCalUpGb] = useState<number | null>(null);
@@ -73,6 +121,7 @@ export default function ProbeMetaSection({ nodeId }: { nodeId: number }) {
const [saving, setSaving] = useState(false);
useEffect(() => {
if (nodeId == null) return;
let cancelled = false;
HttpUtil.get<ProbeMetaWire>(`/panel/api/agent/probe-meta/${nodeId}`, {
silent: true,
@@ -87,12 +136,59 @@ export default function ProbeMetaSection({ nodeId }: { nodeId: number }) {
};
}, [nodeId]);
const meta = loaded && loaded.nodeId === nodeId ? loaded.meta : null;
if (!meta) return null;
const meta = loaded && loaded.nodeId === (nodeId ?? null) ? loaded.meta : null;
const set = <K extends keyof ProbeMetaWire>(key: K, value: ProbeMetaWire[K]) =>
setLoaded((l) => (l ? { nodeId: l.nodeId, meta: { ...l.meta, [key]: value } } : l));
// Full PUT payload from current form state (traffic limit converts
// from the GB box; calibration boxes fold to 0 when empty).
const buildPayload = (): ProbeMetaWire | null => {
if (!meta) return null;
return {
...meta,
trafficLimit: Math.round(trafficGb * BYTES_PER_GB),
trafficCalUpGb: calUpGb ?? 0,
trafficCalDownGb: calDownGb ?? 0,
};
};
// NOTE: hooks must stay above the early return below. In edit mode the
// first render has no meta yet, so a hook placed after the return would
// run on later renders only and React would throw #310 (rendered more
// hooks than during the previous render).
// Create-mode read-out: the parent submits this after the node
// exists. Dirty compares against the defaults the draft started
// from, so an untouched section submits nothing at all.
useImperativeHandle(ref, () => ({
getDraft: () => {
const payload = buildPayload();
if (!payload) return { payload: { ...DEFAULT_PROBE_META }, dirty: false };
const d = DEFAULT_PROBE_META;
const dirty =
payload.price !== d.price ||
payload.currency !== d.currency ||
payload.billingCycle !== d.billingCycle ||
payload.expiresAt !== d.expiresAt ||
payload.trafficLimit !== d.trafficLimit ||
payload.trafficMode !== d.trafficMode ||
payload.trafficResetDay !== d.trafficResetDay ||
payload.public !== d.public ||
payload.sort !== d.sort ||
payload.group !== d.group ||
payload.publicRemark !== d.publicRemark ||
payload.notify !== d.notify ||
payload.countryPin !== d.countryPin ||
payload.ipv4Pin !== d.ipv4Pin ||
payload.ipv6Pin !== d.ipv6Pin ||
payload.trafficCalUpGb !== 0 ||
payload.trafficCalDownGb !== 0 ||
payload.statIface !== d.statIface;
return { payload, dirty };
},
}));
if (!meta) return null;
const cycleLabel = (cycle: string) => {
switch (cycle) {
case 'once':
@@ -122,15 +218,13 @@ export default function ProbeMetaSection({ nodeId }: { nodeId: number }) {
}
async function onSave() {
if (!meta) return;
// Edit mode only: in draft mode the button is hidden and the
// parent submits through the ref handle instead.
if (!meta || nodeId == null) return;
setSaving(true);
try {
const payload: ProbeMetaWire = {
...meta,
trafficLimit: Math.round(trafficGb * BYTES_PER_GB),
trafficCalUpGb: calUpGb ?? 0,
trafficCalDownGb: calDownGb ?? 0,
};
const payload = buildPayload();
if (!payload) return;
const msg = await HttpUtil.put(`/panel/api/agent/probe-meta/${nodeId}`, payload, {
silent: true,
...JSON_HEADERS,
@@ -156,8 +250,6 @@ export default function ProbeMetaSection({ nodeId }: { nodeId: number }) {
return (
<div style={{ marginBottom: 16 }}>
{messageContextHolder}
<div style={{ fontWeight: 600, marginBottom: 4 }}>{t('pages.nodes.probe.title')}</div>
<p style={{ marginBottom: 12 }}>{t('pages.nodes.probe.hint')}</p>
<Row gutter={16}>
<Col xs={24} md={12}>
<Field label={t('pages.nodes.probe.public')}>
@@ -170,13 +262,16 @@ export default function ProbeMetaSection({ nodeId }: { nodeId: number }) {
</Field>
</Col>
</Row>
<Row gutter={16}>
<Col xs={24} md={12}>
<Field label={t('pages.nodes.probe.publicRemark')}>
<Input value={meta.publicRemark} onChange={(e) => set('publicRemark', e.target.value)} />
</Field>
<div style={{ display: 'flex', gap: 16 }}>
<div style={{ flex: 1, minWidth: 0 }}>
<Field label={t('pages.nodes.probe.group')}>
<Input value={meta.group} onChange={(e) => set('group', e.target.value)} />
</Field>
</Col>
<Col xs={24} md={12}>
</div>
<div style={{ flex: 1, minWidth: 0 }}>
<Field label={t('pages.nodes.probe.sort')}>
<InputNumber
value={meta.sort}
@@ -184,13 +279,8 @@ export default function ProbeMetaSection({ nodeId }: { nodeId: number }) {
style={{ width: '100%' }}
/>
</Field>
</Col>
</Row>
<Field label={t('pages.nodes.probe.publicRemark')}>
<Input value={meta.publicRemark} onChange={(e) => set('publicRemark', e.target.value)} />
</Field>
<Row gutter={16}>
<Col xs={24} md={8}>
</div>
<div style={{ flex: 1, minWidth: 0 }}>
<Field label={t('pages.nodes.probe.price')}>
<InputNumber
value={meta.price}
@@ -199,13 +289,13 @@ export default function ProbeMetaSection({ nodeId }: { nodeId: number }) {
style={{ width: '100%' }}
/>
</Field>
</Col>
<Col xs={24} md={8}>
</div>
<div style={{ flex: 1, minWidth: 0 }}>
<Field label={t('pages.nodes.probe.currency')}>
<Input value={meta.currency} onChange={(e) => set('currency', e.target.value)} />
</Field>
</Col>
<Col xs={24} md={8}>
</div>
<div style={{ flex: 1, minWidth: 0 }}>
<Field label={t('pages.nodes.probe.billingCycle')}>
<Select
value={meta.billingCycle}
@@ -214,19 +304,20 @@ export default function ProbeMetaSection({ nodeId }: { nodeId: number }) {
style={{ width: '100%' }}
/>
</Field>
</Col>
</Row>
<Row gutter={16}>
<Col xs={24} md={12}>
</div>
</div>
<div style={{ display: 'flex', gap: 16 }}>
<div style={{ flex: 1, minWidth: 0 }}>
<Field label={t('pages.nodes.probe.expiresAt')}>
<Input
value={meta.expiresAt ?? ''}
placeholder="YYYY-MM-DD"
onChange={(e) => set('expiresAt', e.target.value || null)}
<DatePicker
value={meta.expiresAt ? dayjs(meta.expiresAt) : null}
format="YYYY-MM-DD"
onChange={(d) => set('expiresAt', d ? d.format('YYYY-MM-DD') : null)}
style={{ width: '100%' }}
/>
</Field>
</Col>
<Col xs={24} md={12}>
</div>
<div style={{ flex: 1, minWidth: 0 }}>
<Field label={t('pages.nodes.probe.trafficResetDay')}>
<InputNumber
value={meta.trafficResetDay}
@@ -236,10 +327,8 @@ export default function ProbeMetaSection({ nodeId }: { nodeId: number }) {
style={{ width: '100%' }}
/>
</Field>
</Col>
</Row>
<Row gutter={16}>
<Col xs={24} md={12}>
</div>
<div style={{ flex: 1, minWidth: 0 }}>
<Field label={t('pages.nodes.probe.trafficLimit')}>
<InputNumber
value={trafficGb}
@@ -248,8 +337,8 @@ export default function ProbeMetaSection({ nodeId }: { nodeId: number }) {
style={{ width: '100%' }}
/>
</Field>
</Col>
<Col xs={24} md={12}>
</div>
<div style={{ flex: 1, minWidth: 0 }}>
<Field label={t('pages.nodes.probe.trafficMode')}>
<Select
value={meta.trafficMode}
@@ -263,8 +352,8 @@ export default function ProbeMetaSection({ nodeId }: { nodeId: number }) {
style={{ width: '100%' }}
/>
</Field>
</Col>
</Row>
</div>
</div>
<Row gutter={16}>
<Col xs={24} md={8}>
<Field label={t('pages.nodes.probe.calUp')}>
@@ -316,9 +405,11 @@ export default function ProbeMetaSection({ nodeId }: { nodeId: number }) {
</Field>
</Col>
</Row>
<Button type="primary" loading={saving} onClick={onSave}>
{t('pages.nodes.probe.save')}
</Button>
{!hideSave && (
<Button type="primary" loading={saving} onClick={onSave}>
{t('pages.nodes.probe.save')}
</Button>
)}
</div>
);
}
+24 -24
View File
@@ -230,35 +230,35 @@ export default function SettingsPage() {
)}
<Row gutter={[isMobile ? 8 : 16, isMobile ? 0 : 12]}>
<Col span={24}>
<Card hoverable>
<Row className="header-row">
<Col xs={24} sm={10} className="header-actions">
<Space>
<Button type="primary" disabled={saveDisabled} onClick={onSave}>
{t('pages.settings.save')}
</Button>
<Button
type="primary"
danger
disabled={!saveDisabled}
onClick={restartPanel}
>
{t('pages.settings.restartPanel')}
</Button>
</Space>
</Col>
<Col xs={24} sm={14} className="header-info">
<FloatButton.BackTop target={scrollTarget} visibilityHeight={200} />
</Col>
</Row>
</Card>
</Col>
{activeSlug !== 'public-page' && activeSlug !== 'probe' && (
<Col span={24}>
<Card hoverable>
<Row className="header-row">
<Col xs={24} sm={10} className="header-actions">
<Space>
<Button type="primary" disabled={saveDisabled} onClick={onSave}>
{t('pages.settings.save')}
</Button>
<Button
type="primary"
danger
disabled={!saveDisabled}
onClick={restartPanel}
>
{t('pages.settings.restartPanel')}
</Button>
</Space>
</Col>
</Row>
</Card>
</Col>
)}
<Col span={24}>
<Card hoverable>{categoryBody}</Card>
</Col>
</Row>
<FloatButton.BackTop target={scrollTarget} visibilityHeight={200} />
</>
)}
</Spin>
+1 -1
View File
@@ -22,7 +22,7 @@ esac
# 改名对齐:旧 tag v3.7.0-agent.1 从未携带改名后的 l-ui-linux 资产,沿用它
# 会指向不存在的发行;此处改指本批发版 V2.7.0。仓库名定为 rogue/l-ui,
# Gitea 改名在发版前完成,占位警告随之销账。
LUI_URL="${LUI_URL:-https://git.881218.xyz/rogue/l-ui/releases/download/V2.7.2/l-ui-linux-${BIN_ARCH}}"
LUI_URL="${LUI_URL:-https://git.881218.xyz/rogue/l-ui/releases/download/V2.7.5/l-ui-linux-${BIN_ARCH}}"
XRAY_VER="26.6.27"
info "创建目录..."
+2 -8
View File
@@ -865,7 +865,6 @@
"updateResult": "تم بدء التحديث على {ok} عقدة، فشل {failed}",
"updateNoneEligible": "اختر عقدة واحدة على الأقل متصلة ومفعّلة"
},
"agentHint": "Agent nodes connect out to the master; no address, port or API token needed. An install command is generated on creation.",
"probeOnly": "Probe only",
"probeOnlyHint": "Probe-only mode: no proxy core runs on this node; only monitoring (heartbeat, metrics and latency probes) is active.",
"agentTokenTitle": "Agent Node Setup",
@@ -878,8 +877,6 @@
"agentMasterUrlHint": "Master URL (used by agent to reach the master; change to public address if behind proxy):",
"agentMasterUrlRequired": "Master URL is empty — enter the master address first; the install command is generated only then.",
"probe": {
"title": "Public status page",
"hint": "Display and plan data for the public status page. Saving takes effect immediately.",
"public": "Show on public page",
"publicRemark": "Public remark",
"group": "Group",
@@ -894,8 +891,8 @@
"cycleYearly": "Yearly",
"cycleBiennial": "Biennial",
"cycleTriennial": "Triennial",
"expiresAt": "Expires on (YYYY-MM-DD)",
"trafficLimit": "Traffic limit (GB, 0 = unlimited)",
"expiresAt": "Expires on",
"trafficLimit": "Traffic limit (GB)",
"trafficMode": "Traffic counting",
"modeSum": "Up + down",
"modeUp": "Up only",
@@ -1314,7 +1311,6 @@
"mihomoX25519": "Mihomo X25519",
"shuffleHost": "خلط المضيف",
"tags": "وسوم",
"nodeGuids": "النودز",
"excludeFromSubTypes": "استبعاد من الصيغ",
"verifyPeerCertByName": "التحقق من شهادة النظير بالاسم",
"inheritAddress": "يرث العنوان"
@@ -1322,8 +1318,6 @@
"hints": {
"address": "اتركه فارغاً ليرث عنوان الوارد نفسه.",
"port": "0 يرث منفذ الوارد.",
"tags": "غير مرئي للمستخدمين النهائيين؛ يُرسل مع اشتراك RAW فقط. أحرف كبيرة وأرقام و _ و : فقط.",
"nodeGuids": "اختر النودز التي تم تحليلها من هذا المضيف. تعيين بصري فقط.",
"serverDescription": "ملاحظة اختيارية تظهر تحت الملاحظة.",
"allowInsecure": "تخطّي التحقق من شهادة TLS (allowInsecure / skip-cert-verify).",
"vlessRoute": "قيمة مسار VLESS واحدة (0-65535) تُدمَج في UUID، مثل 443. اتركه فارغاً لعدم وجود أي منها.",
+2 -8
View File
@@ -861,15 +861,12 @@
"mihomoX25519": "Mihomo X25519",
"shuffleHost": "Shuffle host",
"tags": "Tags",
"nodeGuids": "Nodes",
"excludeFromSubTypes": "Exclude from formats",
"inheritAddress": "Inherits"
},
"hints": {
"address": "Leave blank to inherit the inbound's own address.",
"port": "0 inherits the inbound's port.",
"tags": "Not visible to end users; sent with RAW subscription only. Uppercase letters, digits, _ and : only.",
"nodeGuids": "Pick nodes which resolved from this host. Only visual assignment.",
"serverDescription": "Optional note shown under the remark.",
"allowInsecure": "Skip TLS certificate verification (allowInsecure / skip-cert-verify).",
"vlessRoute": "Single VLESS route value (0-65535) baked into the UUID, e.g. 443. Leave blank for none.",
@@ -983,7 +980,6 @@
"updateResult": "Update triggered on {ok} node(s), {failed} failed",
"updateNoneEligible": "Select at least one online, enabled node"
},
"agentHint": "Agent nodes connect out to the master; no address, port or API token needed. An install command is generated on creation.",
"probeOnly": "Probe only",
"probeOnlyHint": "Probe-only mode: no proxy core runs on this node; only monitoring (heartbeat, metrics and latency probes) is active.",
"agentTokenTitle": "Agent Node Setup",
@@ -996,8 +992,6 @@
"agentMasterUrlHint": "Master URL (used by agent to reach the master; change to public address if behind proxy):",
"agentMasterUrlRequired": "Master URL is empty — enter the master address first; the install command is generated only then.",
"probe": {
"title": "Public status page",
"hint": "Display and plan data for the public status page. Saving takes effect immediately.",
"public": "Show on public page",
"publicRemark": "Public remark",
"group": "Group",
@@ -1012,8 +1006,8 @@
"cycleYearly": "Yearly",
"cycleBiennial": "Biennial",
"cycleTriennial": "Triennial",
"expiresAt": "Expires on (YYYY-MM-DD)",
"trafficLimit": "Traffic limit (GB, 0 = unlimited)",
"expiresAt": "Expires on",
"trafficLimit": "Traffic limit (GB)",
"trafficMode": "Traffic counting",
"modeSum": "Up + down",
"modeUp": "Up only",
+2 -8
View File
@@ -865,7 +865,6 @@
"updateResult": "Actualización iniciada en {ok} nodo(s), {failed} fallaron",
"updateNoneEligible": "Selecciona al menos un nodo en línea y habilitado"
},
"agentHint": "Agent nodes connect out to the master; no address, port or API token needed. An install command is generated on creation.",
"probeOnly": "Probe only",
"probeOnlyHint": "Probe-only mode: no proxy core runs on this node; only monitoring (heartbeat, metrics and latency probes) is active.",
"agentTokenTitle": "Agent Node Setup",
@@ -878,8 +877,6 @@
"agentMasterUrlHint": "Master URL (used by agent to reach the master; change to public address if behind proxy):",
"agentMasterUrlRequired": "Master URL is empty — enter the master address first; the install command is generated only then.",
"probe": {
"title": "Public status page",
"hint": "Display and plan data for the public status page. Saving takes effect immediately.",
"public": "Show on public page",
"publicRemark": "Public remark",
"group": "Group",
@@ -894,8 +891,8 @@
"cycleYearly": "Yearly",
"cycleBiennial": "Biennial",
"cycleTriennial": "Triennial",
"expiresAt": "Expires on (YYYY-MM-DD)",
"trafficLimit": "Traffic limit (GB, 0 = unlimited)",
"expiresAt": "Expires on",
"trafficLimit": "Traffic limit (GB)",
"trafficMode": "Traffic counting",
"modeSum": "Up + down",
"modeUp": "Up only",
@@ -1315,15 +1312,12 @@
"mihomoX25519": "Mihomo X25519",
"shuffleHost": "Barajar host",
"tags": "Etiquetas",
"nodeGuids": "Nodos",
"excludeFromSubTypes": "Excluir de formatos",
"inheritAddress": "Hereda dirección"
},
"hints": {
"address": "Déjalo en blanco para heredar la dirección propia del inbound.",
"port": "0 hereda el puerto del inbound.",
"tags": "No visible para los usuarios finales; se envía solo con la suscripción RAW. Solo letras mayúsculas, dígitos, _ y :.",
"nodeGuids": "Elige los nodos que se resolvieron desde este host. Solo asignación visual.",
"serverDescription": "Nota opcional que se muestra bajo las notas.",
"allowInsecure": "Omitir la verificación del certificado TLS (allowInsecure / skip-cert-verify).",
"vlessRoute": "Un único valor de ruta VLESS (0-65535) incrustado en el UUID, p. ej. 443. Déjalo en blanco para ninguno.",
+2 -8
View File
@@ -865,7 +865,6 @@
"updateResult": "به‌روزرسانی روی {ok} نود آغاز شد، {failed} ناموفق",
"updateNoneEligible": "حداقل یک نود آنلاین و فعال انتخاب کنید"
},
"agentHint": "Agent nodes connect out to the master; no address, port or API token needed. An install command is generated on creation.",
"probeOnly": "Probe only",
"probeOnlyHint": "Probe-only mode: no proxy core runs on this node; only monitoring (heartbeat, metrics and latency probes) is active.",
"agentTokenTitle": "Agent Node Setup",
@@ -878,8 +877,6 @@
"agentMasterUrlHint": "Master URL (used by agent to reach the master; change to public address if behind proxy):",
"agentMasterUrlRequired": "Master URL is empty — enter the master address first; the install command is generated only then.",
"probe": {
"title": "Public status page",
"hint": "Display and plan data for the public status page. Saving takes effect immediately.",
"public": "Show on public page",
"publicRemark": "Public remark",
"group": "Group",
@@ -894,8 +891,8 @@
"cycleYearly": "Yearly",
"cycleBiennial": "Biennial",
"cycleTriennial": "Triennial",
"expiresAt": "Expires on (YYYY-MM-DD)",
"trafficLimit": "Traffic limit (GB, 0 = unlimited)",
"expiresAt": "Expires on",
"trafficLimit": "Traffic limit (GB)",
"trafficMode": "Traffic counting",
"modeSum": "Up + down",
"modeUp": "Up only",
@@ -1315,15 +1312,12 @@
"mihomoX25519": "Mihomo X25519",
"shuffleHost": "درهم‌سازی میزبان",
"tags": "برچسب‌ها",
"nodeGuids": "نودها",
"excludeFromSubTypes": "حذف از فرمت‌ها",
"inheritAddress": "ارث‌بری آدرس"
},
"hints": {
"address": "برای ارث‌بری آدرس خودِ اینباند خالی بگذارید.",
"port": "مقدار ۰ پورت اینباند را به ارث می‌برد.",
"tags": "برای کاربران نهایی قابل مشاهده نیست؛ فقط با اشتراک RAW ارسال می‌شود. تنها حروف بزرگ، ارقام، _ و : مجاز است.",
"nodeGuids": "نودهایی را که از این میزبان resolve می‌شوند انتخاب کنید. صرفاً انتساب نمایشی است.",
"serverDescription": "یادداشت اختیاری که زیر نام نمایش داده می‌شود.",
"allowInsecure": "رد کردن بررسی گواهی TLS (allowInsecure / skip-cert-verify).",
"vlessRoute": "یک مقدار مسیر VLESS (0 تا 65535) که در UUID جاسازی می‌شود، مثلاً 443. برای هیچ‌کدام خالی بگذارید.",
+2 -8
View File
@@ -865,7 +865,6 @@
"updateResult": "Pembaruan dipicu pada {ok} node, {failed} gagal",
"updateNoneEligible": "Pilih minimal satu node online dan aktif"
},
"agentHint": "Agent nodes connect out to the master; no address, port or API token needed. An install command is generated on creation.",
"probeOnly": "Probe only",
"probeOnlyHint": "Probe-only mode: no proxy core runs on this node; only monitoring (heartbeat, metrics and latency probes) is active.",
"agentTokenTitle": "Agent Node Setup",
@@ -878,8 +877,6 @@
"agentMasterUrlHint": "Master URL (used by agent to reach the master; change to public address if behind proxy):",
"agentMasterUrlRequired": "Master URL is empty — enter the master address first; the install command is generated only then.",
"probe": {
"title": "Public status page",
"hint": "Display and plan data for the public status page. Saving takes effect immediately.",
"public": "Show on public page",
"publicRemark": "Public remark",
"group": "Group",
@@ -894,8 +891,8 @@
"cycleYearly": "Yearly",
"cycleBiennial": "Biennial",
"cycleTriennial": "Triennial",
"expiresAt": "Expires on (YYYY-MM-DD)",
"trafficLimit": "Traffic limit (GB, 0 = unlimited)",
"expiresAt": "Expires on",
"trafficLimit": "Traffic limit (GB)",
"trafficMode": "Traffic counting",
"modeSum": "Up + down",
"modeUp": "Up only",
@@ -1314,7 +1311,6 @@
"mihomoX25519": "Mihomo X25519",
"shuffleHost": "Acak host",
"tags": "Tag",
"nodeGuids": "Node",
"excludeFromSubTypes": "Kecualikan dari format",
"verifyPeerCertByName": "Verifikasi sertifikat peer berdasarkan nama",
"inheritAddress": "Warisi alamat"
@@ -1322,8 +1318,6 @@
"hints": {
"address": "Biarkan kosong untuk mewarisi alamat inbound itu sendiri.",
"port": "0 mewarisi port inbound.",
"tags": "Tidak terlihat oleh pengguna akhir; hanya dikirim dengan langganan RAW. Hanya huruf kapital, angka, _ dan :.",
"nodeGuids": "Pilih node yang teresolusi dari host ini. Hanya penetapan visual.",
"serverDescription": "Catatan opsional yang ditampilkan di bawah catatan.",
"allowInsecure": "Lewati verifikasi sertifikat TLS (allowInsecure / skip-cert-verify).",
"vlessRoute": "Satu nilai rute VLESS (0-65535) yang disisipkan ke UUID, mis. 443. Biarkan kosong jika tidak ada.",
+2 -8
View File
@@ -865,7 +865,6 @@
"updateResult": "{ok} 個のノードで更新を開始、{failed} 個失敗",
"updateNoneEligible": "オンラインで有効なノードを少なくとも1つ選択してください"
},
"agentHint": "Agent nodes connect out to the master; no address, port or API token needed. An install command is generated on creation.",
"probeOnly": "Probe only",
"probeOnlyHint": "Probe-only mode: no proxy core runs on this node; only monitoring (heartbeat, metrics and latency probes) is active.",
"agentTokenTitle": "Agent Node Setup",
@@ -878,8 +877,6 @@
"agentMasterUrlHint": "Master URL (used by agent to reach the master; change to public address if behind proxy):",
"agentMasterUrlRequired": "Master URL is empty — enter the master address first; the install command is generated only then.",
"probe": {
"title": "Public status page",
"hint": "Display and plan data for the public status page. Saving takes effect immediately.",
"public": "Show on public page",
"publicRemark": "Public remark",
"group": "Group",
@@ -894,8 +891,8 @@
"cycleYearly": "Yearly",
"cycleBiennial": "Biennial",
"cycleTriennial": "Triennial",
"expiresAt": "Expires on (YYYY-MM-DD)",
"trafficLimit": "Traffic limit (GB, 0 = unlimited)",
"expiresAt": "Expires on",
"trafficLimit": "Traffic limit (GB)",
"trafficMode": "Traffic counting",
"modeSum": "Up + down",
"modeUp": "Up only",
@@ -1315,15 +1312,12 @@
"mihomoX25519": "Mihomo X25519",
"shuffleHost": "ホストをシャッフル",
"tags": "タグ",
"nodeGuids": "ノード",
"excludeFromSubTypes": "形式から除外",
"inheritAddress": "アドレス継承"
},
"hints": {
"address": "空欄にするとインバウンド自身のアドレスを継承します。",
"port": "0 にするとインバウンドのポートを継承します。",
"tags": "エンドユーザーには表示されません。RAW サブスクリプションでのみ送信されます。大文字、数字、_ と : のみ使用できます。",
"nodeGuids": "このホストから解決されたノードを選択します。視覚的な割り当てのみです。",
"serverDescription": "備考の下に表示される任意のメモ。",
"allowInsecure": "TLS 証明書の検証をスキップします(allowInsecure / skip-cert-verify)。",
"vlessRoute": "UUID に埋め込まれる単一の VLESS ルート値(0〜65535)。例: 443。なしの場合は空欄にします。",
+2 -8
View File
@@ -865,7 +865,6 @@
"updateResult": "Atualização iniciada em {ok} nó(s), {failed} falharam",
"updateNoneEligible": "Selecione pelo menos um nó online e ativo"
},
"agentHint": "Agent nodes connect out to the master; no address, port or API token needed. An install command is generated on creation.",
"probeOnly": "Probe only",
"probeOnlyHint": "Probe-only mode: no proxy core runs on this node; only monitoring (heartbeat, metrics and latency probes) is active.",
"agentTokenTitle": "Agent Node Setup",
@@ -878,8 +877,6 @@
"agentMasterUrlHint": "Master URL (used by agent to reach the master; change to public address if behind proxy):",
"agentMasterUrlRequired": "Master URL is empty — enter the master address first; the install command is generated only then.",
"probe": {
"title": "Public status page",
"hint": "Display and plan data for the public status page. Saving takes effect immediately.",
"public": "Show on public page",
"publicRemark": "Public remark",
"group": "Group",
@@ -894,8 +891,8 @@
"cycleYearly": "Yearly",
"cycleBiennial": "Biennial",
"cycleTriennial": "Triennial",
"expiresAt": "Expires on (YYYY-MM-DD)",
"trafficLimit": "Traffic limit (GB, 0 = unlimited)",
"expiresAt": "Expires on",
"trafficLimit": "Traffic limit (GB)",
"trafficMode": "Traffic counting",
"modeSum": "Up + down",
"modeUp": "Up only",
@@ -1315,15 +1312,12 @@
"mihomoX25519": "Mihomo X25519",
"shuffleHost": "Embaralhar host",
"tags": "Tags",
"nodeGuids": "Nós",
"excludeFromSubTypes": "Excluir dos formatos",
"inheritAddress": "Herda endereço"
},
"hints": {
"address": "Deixe em branco para herdar o próprio endereço da entrada.",
"port": "0 herda a porta da entrada.",
"tags": "Não visível aos usuários finais; enviado apenas na assinatura RAW. Apenas letras maiúsculas, dígitos, _ e :.",
"nodeGuids": "Escolha os nós que foram resolvidos a partir deste host. Apenas atribuição visual.",
"serverDescription": "Nota opcional exibida abaixo da observação.",
"allowInsecure": "Ignorar a verificação do certificado TLS (allowInsecure / skip-cert-verify).",
"vlessRoute": "Um único valor de rota VLESS (0-65535) embutido no UUID, ex.: 443. Deixe em branco para nenhum.",
+2 -8
View File
@@ -865,7 +865,6 @@
"updateResult": "Обновление запущено на {ok} узлах, {failed} не удалось",
"updateNoneEligible": "Выберите хотя бы один включённый узел в сети"
},
"agentHint": "Agent nodes connect out to the master; no address, port or API token needed. An install command is generated on creation.",
"probeOnly": "Probe only",
"probeOnlyHint": "Probe-only mode: no proxy core runs on this node; only monitoring (heartbeat, metrics and latency probes) is active.",
"agentTokenTitle": "Agent Node Setup",
@@ -878,8 +877,6 @@
"agentMasterUrlHint": "Master URL (used by agent to reach the master; change to public address if behind proxy):",
"agentMasterUrlRequired": "Master URL is empty — enter the master address first; the install command is generated only then.",
"probe": {
"title": "Public status page",
"hint": "Display and plan data for the public status page. Saving takes effect immediately.",
"public": "Show on public page",
"publicRemark": "Public remark",
"group": "Group",
@@ -894,8 +891,8 @@
"cycleYearly": "Yearly",
"cycleBiennial": "Biennial",
"cycleTriennial": "Triennial",
"expiresAt": "Expires on (YYYY-MM-DD)",
"trafficLimit": "Traffic limit (GB, 0 = unlimited)",
"expiresAt": "Expires on",
"trafficLimit": "Traffic limit (GB)",
"trafficMode": "Traffic counting",
"modeSum": "Up + down",
"modeUp": "Up only",
@@ -1315,15 +1312,12 @@
"mihomoX25519": "Mihomo X25519",
"shuffleHost": "Перемешивать хост",
"tags": "Теги",
"nodeGuids": "Узлы",
"excludeFromSubTypes": "Исключить из форматов",
"inheritAddress": "Наследует адрес"
},
"hints": {
"address": "Оставьте пустым, чтобы унаследовать собственный адрес входящего.",
"port": "0 наследует порт входящего.",
"tags": "Не видны конечным пользователям; отправляются только с RAW-подпиской. Только заглавные буквы, цифры, _ и :.",
"nodeGuids": "Выберите узлы, которые разрешаются с этого хоста. Только визуальное назначение.",
"serverDescription": "Необязательная заметка, отображаемая под примечанием.",
"allowInsecure": "Пропустить проверку TLS-сертификата (allowInsecure / skip-cert-verify).",
"vlessRoute": "Одно значение маршрута VLESS (0-65535), встраиваемое в UUID, напр. 443. Оставьте пустым, чтобы отключить.",
+2 -8
View File
@@ -865,7 +865,6 @@
"updateResult": "{ok} düğümde güncelleme başlatıldı, {failed} başarısız",
"updateNoneEligible": "En az bir çevrimiçi ve etkin düğüm seçin"
},
"agentHint": "Agent nodes connect out to the master; no address, port or API token needed. An install command is generated on creation.",
"probeOnly": "Probe only",
"probeOnlyHint": "Probe-only mode: no proxy core runs on this node; only monitoring (heartbeat, metrics and latency probes) is active.",
"agentTokenTitle": "Agent Node Setup",
@@ -878,8 +877,6 @@
"agentMasterUrlHint": "Master URL (used by agent to reach the master; change to public address if behind proxy):",
"agentMasterUrlRequired": "Master URL is empty — enter the master address first; the install command is generated only then.",
"probe": {
"title": "Public status page",
"hint": "Display and plan data for the public status page. Saving takes effect immediately.",
"public": "Show on public page",
"publicRemark": "Public remark",
"group": "Group",
@@ -894,8 +891,8 @@
"cycleYearly": "Yearly",
"cycleBiennial": "Biennial",
"cycleTriennial": "Triennial",
"expiresAt": "Expires on (YYYY-MM-DD)",
"trafficLimit": "Traffic limit (GB, 0 = unlimited)",
"expiresAt": "Expires on",
"trafficLimit": "Traffic limit (GB)",
"trafficMode": "Traffic counting",
"modeSum": "Up + down",
"modeUp": "Up only",
@@ -1314,7 +1311,6 @@
"mihomoX25519": "Mihomo X25519",
"shuffleHost": "Host'u karıştır",
"tags": "Etiketler",
"nodeGuids": "Düğümler",
"excludeFromSubTypes": "Formatlardan hariç tut",
"verifyPeerCertByName": "Peer sertifikasını ada göre doğrula",
"inheritAddress": "Adresi devralır"
@@ -1322,8 +1318,6 @@
"hints": {
"address": "Gelen bağlantının kendi adresini devralmak için boş bırakın.",
"port": "0 değeri gelen bağlantının portunu devralır.",
"tags": "Son kullanıcılara görünmez; yalnızca RAW abonelikle gönderilir. Yalnızca büyük harf, rakam, _ ve : kullanılabilir.",
"nodeGuids": "Bu host'tan çözümlenen düğümleri seçin. Yalnızca görsel atama.",
"serverDescription": "Açıklamanın altında gösterilen isteğe bağlı not.",
"allowInsecure": "TLS sertifika doğrulamasını atla (allowInsecure / skip-cert-verify).",
"vlessRoute": "UUID'ye gömülen tek bir VLESS rota değeri (0-65535), örn. 443. Hiçbiri için boş bırakın.",
+2 -8
View File
@@ -865,7 +865,6 @@
"updateResult": "Оновлення запущено на {ok} вузлах, {failed} не вдалося",
"updateNoneEligible": "Виберіть принаймні один увімкнений вузол у мережі"
},
"agentHint": "Agent nodes connect out to the master; no address, port or API token needed. An install command is generated on creation.",
"probeOnly": "Probe only",
"probeOnlyHint": "Probe-only mode: no proxy core runs on this node; only monitoring (heartbeat, metrics and latency probes) is active.",
"agentTokenTitle": "Agent Node Setup",
@@ -878,8 +877,6 @@
"agentMasterUrlHint": "Master URL (used by agent to reach the master; change to public address if behind proxy):",
"agentMasterUrlRequired": "Master URL is empty — enter the master address first; the install command is generated only then.",
"probe": {
"title": "Public status page",
"hint": "Display and plan data for the public status page. Saving takes effect immediately.",
"public": "Show on public page",
"publicRemark": "Public remark",
"group": "Group",
@@ -894,8 +891,8 @@
"cycleYearly": "Yearly",
"cycleBiennial": "Biennial",
"cycleTriennial": "Triennial",
"expiresAt": "Expires on (YYYY-MM-DD)",
"trafficLimit": "Traffic limit (GB, 0 = unlimited)",
"expiresAt": "Expires on",
"trafficLimit": "Traffic limit (GB)",
"trafficMode": "Traffic counting",
"modeSum": "Up + down",
"modeUp": "Up only",
@@ -1315,15 +1312,12 @@
"mihomoX25519": "Mihomo X25519",
"shuffleHost": "Перемішувати host",
"tags": "Теги",
"nodeGuids": "Вузли",
"excludeFromSubTypes": "Виключити з форматів",
"inheritAddress": "Успадковує адресу"
},
"hints": {
"address": "Залиште порожнім, щоб успадкувати власну адресу вхідного.",
"port": "0 успадковує порт вхідного.",
"tags": "Не видно кінцевим користувачам; надсилається лише з RAW-підпискою. Лише великі літери, цифри, _ та :.",
"nodeGuids": "Виберіть вузли, які розв'язуються з цього хоста. Лише візуальне призначення.",
"serverDescription": "Необов'язкова примітка, що показується під приміткою.",
"allowInsecure": "Пропускати перевірку TLS-сертифіката (allowInsecure / skip-cert-verify).",
"vlessRoute": "Одне значення маршруту VLESS (0-65535), що вбудовується в UUID, напр. 443. Залиште порожнім, щоб не використовувати.",
+2 -8
View File
@@ -865,7 +865,6 @@
"updateResult": "Đã kích hoạt cập nhật trên {ok} node, {failed} thất bại",
"updateNoneEligible": "Chọn ít nhất một node trực tuyến và đang bật"
},
"agentHint": "Agent nodes connect out to the master; no address, port or API token needed. An install command is generated on creation.",
"probeOnly": "Probe only",
"probeOnlyHint": "Probe-only mode: no proxy core runs on this node; only monitoring (heartbeat, metrics and latency probes) is active.",
"agentTokenTitle": "Agent Node Setup",
@@ -878,8 +877,6 @@
"agentMasterUrlHint": "Master URL (used by agent to reach the master; change to public address if behind proxy):",
"agentMasterUrlRequired": "Master URL is empty — enter the master address first; the install command is generated only then.",
"probe": {
"title": "Public status page",
"hint": "Display and plan data for the public status page. Saving takes effect immediately.",
"public": "Show on public page",
"publicRemark": "Public remark",
"group": "Group",
@@ -894,8 +891,8 @@
"cycleYearly": "Yearly",
"cycleBiennial": "Biennial",
"cycleTriennial": "Triennial",
"expiresAt": "Expires on (YYYY-MM-DD)",
"trafficLimit": "Traffic limit (GB, 0 = unlimited)",
"expiresAt": "Expires on",
"trafficLimit": "Traffic limit (GB)",
"trafficMode": "Traffic counting",
"modeSum": "Up + down",
"modeUp": "Up only",
@@ -1315,15 +1312,12 @@
"mihomoX25519": "Mihomo X25519",
"shuffleHost": "Xáo trộn host",
"tags": "Tag",
"nodeGuids": "Nút",
"excludeFromSubTypes": "Loại trừ khỏi định dạng",
"inheritAddress": "Kế thừa địa chỉ"
},
"hints": {
"address": "Để trống để kế thừa địa chỉ của chính inbound.",
"port": "0 sẽ kế thừa cổng của inbound.",
"tags": "Không hiển thị cho người dùng cuối; chỉ gửi kèm đăng ký RAW. Chỉ chữ in hoa, chữ số, _ và :.",
"nodeGuids": "Chọn các nút được phân giải từ host này. Chỉ là gán trực quan.",
"serverDescription": "Ghi chú tùy chọn hiển thị bên dưới ghi chú.",
"allowInsecure": "Bỏ qua xác minh chứng chỉ TLS (allowInsecure / skip-cert-verify).",
"vlessRoute": "Một giá trị định tuyến VLESS (0-65535) được nhúng vào UUID, ví dụ 443. Để trống nếu không dùng.",
+2 -8
View File
@@ -865,7 +865,6 @@
"updateResult": "已在 {ok} 个节点上触发更新,{failed} 个失败",
"updateNoneEligible": "请至少选择一个在线且已启用的节点"
},
"agentHint": "Agent 节点由节点机主动连接主控,无需填写地址、端口和 API 令牌。创建后会生成安装命令。",
"probeOnly": "纯探针",
"probeOnlyHint": "纯探针模式:该节点不运行任何代理核心,只保留探针功能(心跳、指标与延迟探测)。",
"agentTokenTitle": "Agent 节点安装",
@@ -878,8 +877,6 @@
"agentMasterUrlHint": "主控地址(agent 用来连接主控,如有域名或反代请改成对外地址):",
"agentMasterUrlRequired": "主控地址为空——请先填入主控地址,之后才会生成安装命令。",
"probe": {
"title": "公开状态页",
"hint": "公开状态页的展示与套餐信息,保存后立即生效。",
"public": "在公开页显示",
"publicRemark": "公开备注",
"group": "分组",
@@ -894,8 +891,8 @@
"cycleYearly": "每年",
"cycleBiennial": "每两年",
"cycleTriennial": "每三年",
"expiresAt": "到期日期(YYYY-MM-DD)",
"trafficLimit": "流量上限(GB,0 = 不限)",
"expiresAt": "到期日期",
"trafficLimit": "流量上限(GB)",
"trafficMode": "流量统计口径",
"modeSum": "上行 + 下行",
"modeUp": "只算上行",
@@ -1315,15 +1312,12 @@
"mihomoX25519": "Mihomo X25519",
"shuffleHost": "随机打乱 Host",
"tags": "标签",
"nodeGuids": "节点",
"excludeFromSubTypes": "从格式中排除",
"inheritAddress": "继承地址"
},
"hints": {
"address": "留空则继承入站自身的地址。",
"port": "填 0 则继承入站的端口。",
"tags": "对终端用户不可见;仅随 RAW 订阅发送。只能包含大写字母、数字、_ 和 :。",
"nodeGuids": "选择从此主机解析的节点。仅用于可视化关联。",
"serverDescription": "可选备注,显示在备注下方。",
"allowInsecure": "跳过 TLS 证书验证(allowInsecure / skip-cert-verify)。",
"vlessRoute": "嵌入 UUID 的单个 VLESS 路由值(0-65535),例如 443。留空表示不路由。",
+2 -8
View File
@@ -865,7 +865,6 @@
"updateResult": "已在 {ok} 個節點上觸發更新,{failed} 個失敗",
"updateNoneEligible": "請至少選擇一個在線且已啟用的節點"
},
"agentHint": "Agent nodes connect out to the master; no address, port or API token needed. An install command is generated on creation.",
"probeOnly": "Probe only",
"probeOnlyHint": "Probe-only mode: no proxy core runs on this node; only monitoring (heartbeat, metrics and latency probes) is active.",
"agentTokenTitle": "Agent Node Setup",
@@ -878,8 +877,6 @@
"agentMasterUrlHint": "Master URL (used by agent to reach the master; change to public address if behind proxy):",
"agentMasterUrlRequired": "Master URL is empty — enter the master address first; the install command is generated only then.",
"probe": {
"title": "Public status page",
"hint": "Display and plan data for the public status page. Saving takes effect immediately.",
"public": "Show on public page",
"publicRemark": "Public remark",
"group": "Group",
@@ -894,8 +891,8 @@
"cycleYearly": "Yearly",
"cycleBiennial": "Biennial",
"cycleTriennial": "Triennial",
"expiresAt": "Expires on (YYYY-MM-DD)",
"trafficLimit": "Traffic limit (GB, 0 = unlimited)",
"expiresAt": "Expires on",
"trafficLimit": "Traffic limit (GB)",
"trafficMode": "Traffic counting",
"modeSum": "Up + down",
"modeUp": "Up only",
@@ -1315,15 +1312,12 @@
"mihomoX25519": "Mihomo X25519",
"shuffleHost": "隨機排序 Host",
"tags": "標籤",
"nodeGuids": "節點",
"excludeFromSubTypes": "從格式中排除",
"inheritAddress": "繼承地址"
},
"hints": {
"address": "留空以繼承入站本身的地址。",
"port": "0 表示繼承入站的連接埠。",
"tags": "對終端使用者不可見;僅隨 RAW 訂閱傳送。只能使用大寫字母、數字、_ 和 :。",
"nodeGuids": "選擇由此 Host 解析而來的節點。僅為視覺上的指派。",
"serverDescription": "顯示於備註下方的選填註記。",
"allowInsecure": "略過 TLS 憑證驗證(allowInsecure / skip-cert-verify)。",
"vlessRoute": "嵌入 UUID 的單一 VLESS 路由值(0-65535),例如 443。留空表示無。",
+2 -2
View File
@@ -343,7 +343,7 @@ reset_user() {
echo -e "Panel login username has been reset to: ${green} ${config_account} ${plain}"
echo -e "Panel login password has been reset to: ${green} ${config_password} ${plain}"
echo -e "${green} Please use the new login username and password to access the X-UI panel. Also remember them! ${plain}"
echo -e "${green} Please use the new login username and password to access the L-UI panel. Also remember them! ${plain}"
confirm_restart
}
@@ -3412,7 +3412,7 @@ show_usage() {
show_menu() {
echo -e "
╔────────────────────────────────────────────────╗
│ ${green}3X-UI Panel Management Script${plain} │
│ ${green}L-UI Panel Management Script${plain} │
│ ${green}0.${plain} Exit Script │
│────────────────────────────────────────────────│
│ ${green}1.${plain} Install │