mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-08-27 05:27:14 +00:00
Compare commits
38 Commits
380aff4d82
...
dev-latest
| Author | SHA1 | Date | |
|---|---|---|---|
| fcf60eb2e2 | |||
| 103b0dfe8d | |||
| 2d30ab3ada | |||
| d175050f2e | |||
| 7a595cb46d | |||
| f13baa9af5 | |||
| 9408424959 | |||
| f204997c98 | |||
| effcccceac | |||
| d9b599b9aa | |||
| cc245a908e | |||
| c26ff59b47 | |||
| 6f7a305239 | |||
| da01b7637d | |||
| 81fcacab11 | |||
| 02002dc1c3 | |||
| 326009e9d3 | |||
| 585f4ecdc0 | |||
| bd6a6aba43 | |||
| a3e617215c | |||
| a255ab7c65 | |||
| af3e6c11b6 | |||
| b73ceae081 | |||
| 1250fbb734 | |||
| 5321665d5b | |||
| 73a971c2d1 | |||
| 19a2c23c01 | |||
| e4798a027c | |||
| 845abc380e | |||
| 58669f6146 | |||
| 19e71d9acc | |||
| f7db247b07 | |||
| c8a3a2d723 | |||
| b51f09768b | |||
| 3c087f6fd9 | |||
| ce63bf3e66 | |||
| b9eda09da9 | |||
| 92fb94d856 |
@@ -0,0 +1,181 @@
|
||||
# Repository context for the Claude bot
|
||||
|
||||
Shared briefing for the jobs in `.github/workflows/claude-bot.yml`. It exists so
|
||||
these facts live in ONE place next to the code instead of being restated in each
|
||||
prompt, where they went stale silently. (Pull-request review is separate: its
|
||||
code-review skill is briefed with `CLAUDE.md` and `REVIEW.md`, not this.)
|
||||
|
||||
`CLAUDE.md`, `frontend/CLAUDE.md` and `docs/architecture.md` outrank this file.
|
||||
Where they disagree with it, they win and this file is the thing to fix.
|
||||
`docs/architecture.md` carries a "Symptom -> File" index and the cron-job table,
|
||||
which answer "which file owns X" in one hop; grepping blind wastes turns on a
|
||||
question it already answers.
|
||||
|
||||
## Stack
|
||||
|
||||
3x-ui is an open-source web control panel for managing Xray-core servers.
|
||||
|
||||
- Backend: Go 1.27, module `github.com/mhsanaei/3x-ui/v3`, Gin and GORM.
|
||||
- It runs Xray-core as a managed child process (`internal/xray/process.go`) and
|
||||
imports `github.com/xtls/xray-core` for config types and the gRPC
|
||||
stats/handler/router API. The release the panel BUNDLES is pinned in
|
||||
`DockerInit.sh`; the version it COMPILES against is pinned in `go.mod`, and
|
||||
the two are not always the same.
|
||||
- MTProto inbounds run a SECOND managed child, the `mtg-multi` binary (a
|
||||
multi-secret mtg fork, panel-side code in `internal/mtproto/`), one process
|
||||
per inbound. Client, ad-tag and quota/expiry edits are hot-applied through the
|
||||
fork's management API (`PUT /secrets`) so connections survive, with a process
|
||||
restart as the fallback on older binaries.
|
||||
- Storage: SQLite by default (`/etc/x-ui/x-ui.db` on Linux, the executable
|
||||
directory on Windows) or PostgreSQL (`XUI_DB_TYPE` / `XUI_DB_DSN`). The SQLite
|
||||
driver is CGo, so `CGO_ENABLED=0` builds fail.
|
||||
- Frontend: React 19 + Ant Design 6 + Vite 8 + TypeScript in `frontend/`, built
|
||||
into `internal/web/dist/` (gitignored) and embedded with `embed.FS`.
|
||||
|
||||
## Where things live
|
||||
|
||||
| area | path |
|
||||
| --- | --- |
|
||||
| entry point + `x-ui` CLI | `main.go` |
|
||||
| env parsing | `internal/config/` |
|
||||
| schema, migrations | `internal/database/`, `internal/database/model/` |
|
||||
| Xray child process + config | `internal/xray/` |
|
||||
| MTProto inbounds | `internal/mtproto/` |
|
||||
| subscription server | `internal/sub/` |
|
||||
| HTTP handlers | `internal/web/controller/` |
|
||||
| business logic | `internal/web/service/` |
|
||||
| cron jobs (schedules in `web.go startTask()`) | `internal/web/job/` |
|
||||
| master/sub-node over mTLS | `internal/web/runtime/` |
|
||||
| i18n | `internal/web/locale/`, `internal/web/translation/` |
|
||||
| UI source | `frontend/src/` |
|
||||
| install / upgrade | `install.sh`, `x-ui.sh`, `DockerInit.sh` |
|
||||
|
||||
## Hard rules a change must respect
|
||||
|
||||
- **Dispatch through `runtime.Runtime`.** Every state-changing inbound or client
|
||||
operation goes through the interface in `internal/web/runtime/`, never
|
||||
straight to `internal/xray/api.go`. A direct call passes every local test and
|
||||
silently breaks every multi-node deployment; it is invisible in a single-box
|
||||
reading of a diff.
|
||||
- **Layering.** Controllers are thin — bind, validate, respond — with no GORM
|
||||
queries, no Xray calls and no business rules. `internal/util/*` is leaf-only
|
||||
and must not import service, controller or database. `internal/web/dist/` and
|
||||
`frontend/src/generated/` are generated; a hand-edit is a violation.
|
||||
- **Comments in committed Go/TS/TSX: 2 lines MAX per block**, spent on the *why*
|
||||
a name cannot hold — an invariant, an issue number, a non-obvious constraint.
|
||||
Exempt, never flag: `//go:build`, `//go:generate`, `//nolint:`,
|
||||
`// Code generated ... DO NOT EDIT.`. HTML `<!-- -->` is fine.
|
||||
- **The route contract chain**, which breaks in four distinct places:
|
||||
1. a new `g.POST`/`g.GET` in `internal/web/controller/` needs a matching entry
|
||||
in `frontend/src/pages/api-docs/endpoints.ts` — pinned BOTH ways by
|
||||
`TestRouteRegistryContract` in `internal/web/routes_contract_test.go`, so a
|
||||
renamed or removed route that leaves a stale entry fails too;
|
||||
2. generated artefacts must be regenerated with `make gen`, or CI's `codegen`
|
||||
job fails on a dirty `frontend/src/generated` or
|
||||
`frontend/public/openapi.json`;
|
||||
3. a NEW struct crossing the API boundary must be added to the `StructAllow`
|
||||
allowlist in `tools/openapigen/main.go`, or it is SILENTLY dropped from the
|
||||
schemas and `frontend/scripts/build-openapi.mjs` then fails — a guaranteed
|
||||
CI break, not a style nit;
|
||||
4. the step NOTHING checks — `frontend/public/openapi.json` must be copied to
|
||||
`docs/public/openapi.json` and the MDX regenerated with
|
||||
`cd docs && pnpm gen:api`, because `docs-ci.yml` fires only on `docs/**`.
|
||||
Step 4 is the one that reaches production wrong.
|
||||
- **i18n.** A new English key goes in EVERY locale JSON in
|
||||
`internal/web/translation/` (13 files) AND must be referenced from
|
||||
`frontend/src` or Go in the SAME change.
|
||||
`frontend/src/test/i18n-dead-keys.test.ts` fails on a missing locale file and
|
||||
on an orphan key alike.
|
||||
- **Migrations.** Schema changes are GORM `AutoMigrate` PLUS hand-written
|
||||
migrations in `internal/database/db.go`. There are no migration files and no
|
||||
down-migrations, and everything has to work on SQLite AND PostgreSQL.
|
||||
- **Tests.** Stdlib `testing` only (no testify), table-driven with `t.Run`
|
||||
subtests and `t.Helper()` on helpers. An assertion must pin the exact value,
|
||||
typed error or emitted string — `err != nil` and `len(x) > 0` are findings,
|
||||
not nits. Prefer real dependencies: a throwaway DB via
|
||||
`database.InitDB(filepath.Join(t.TempDir(), "x-ui.db"))` with `t.Cleanup`, and
|
||||
`httptest` for HTTP. `internal/sub`'s `initSubDB(t)` is the template.
|
||||
A test must FAIL without its fix; one that passes either way certifies
|
||||
nothing and then gets cited as proof the fix works.
|
||||
|
||||
## The three link implementations
|
||||
|
||||
Link and subscription generation is implemented three times, independently:
|
||||
|
||||
| language | path | what it feeds |
|
||||
| --- | --- | --- |
|
||||
| Go | `internal/util/link/`, `internal/sub/` | what the panel serves |
|
||||
| TS | `frontend/src/lib/xray/` | what the panel UI shows |
|
||||
| TS | `docs/lib/xray/` | what the docs site shows |
|
||||
|
||||
A change to share-link or subscription output that touches one and not the
|
||||
others is how they drift apart.
|
||||
|
||||
## Downstream programs that must accept what the panel emits
|
||||
|
||||
- **XTLS/Xray-core** — the Xray config the panel generates, and the VLESS/VMess
|
||||
transport and security fields.
|
||||
- **MetaCubeX/mihomo** — consumes the Clash YAML from `internal/sub/`.
|
||||
- **SagerNet/sing-box** — parses the share links the panel emits.
|
||||
- **mhsanaei/mtg-multi** — the MTProto sidecar whose TOML (`[secrets]`,
|
||||
`[secret-ad-tags]`, `[secret-limits]`) and management API
|
||||
(`PUT /secrets`, `POST /secrets/{name}/reset-quota`) `internal/mtproto/`
|
||||
writes and calls.
|
||||
|
||||
## What CI runs
|
||||
|
||||
`.github/workflows/ci.yml`, on every pull request touching Go or frontend code.
|
||||
It is paths-filtered, so a docs-only or workflow-only change produces no run.
|
||||
|
||||
| job | what it proves |
|
||||
| --- | --- |
|
||||
| `go-test` | `go test -shuffle=on -count=1` over every package except `frontend/node_modules` |
|
||||
| `race` | the same set under `-race -shuffle=on` |
|
||||
| `postgres-durable-first` | live PostgreSQL 16: the `PostgresCommitFailure` tests plus `TestHostAutoMigrateCreatesColumns_Postgres` and `TestMigrate_Postgres`. Both steps COUNT passes rather than assert on SKIP, so a renamed or deleted test fails the job |
|
||||
| `govulncheck` | known vulnerabilities |
|
||||
| `golangci` | `golangci-lint` |
|
||||
| `fuzz-smoke` | 30s each on `FuzzParseLink` and `FuzzDecodeCertPin` |
|
||||
| `codegen` | `npm run gen` then `git diff --exit-code` on the generated files |
|
||||
| `frontend` | MSW worker drift, lint, format:check, typecheck, `npm test` (Vitest + headless-Chromium Storybook), build, build-storybook, `npm audit` |
|
||||
|
||||
**What CI does NOT prove.** These test families `t.Skip` unless an environment
|
||||
variable is set, and CI sets only the PostgreSQL ones above:
|
||||
|
||||
| gate | covers |
|
||||
| --- | --- |
|
||||
| `XUI_TEST_PG_DSN` | PostgreSQL-specific paths |
|
||||
| `XUI_DB_TYPE` + `XUI_DB_DSN` | dialect-dependent behaviour |
|
||||
| `XRAY_E2E_BINARY` | the Xray gRPC end-to-end tests in `internal/xray/` |
|
||||
| `XUI_SCALE_TEST` | scale tests in `internal/sub/`, `internal/web/job/`, `internal/web/service/` |
|
||||
|
||||
Mutation testing (`mutation.yml`) runs nightly and never on a pull request, so a
|
||||
test that cannot fail is invisible to CI. `make verify` is the local gate.
|
||||
|
||||
## Support facts reporters get wrong
|
||||
|
||||
- Linux install: `bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh)`
|
||||
- Install generates a RANDOM username, password and web base path — never
|
||||
admin/admin. The `x-ui` menu on the server shows or resets them.
|
||||
- The installer service environment file is DISTRO-DEPENDENT:
|
||||
`/etc/default/x-ui` (Debian/Ubuntu), `/etc/conf.d/x-ui` (Arch),
|
||||
`/etc/sysconfig/x-ui` (RHEL/Fedora). Naming the wrong one means the reporter's
|
||||
edit is silently never read by systemd — a common cause of "I set the variable
|
||||
and nothing happened".
|
||||
- Windows is supported. There the database sits next to the executable, not in
|
||||
`/etc` — never quote the Linux path to a Windows user.
|
||||
- SQLite to PostgreSQL: `x-ui migrate-db --dsn "postgres://..."`, then set
|
||||
`XUI_DB_TYPE`/`XUI_DB_DSN` in that file and `systemctl restart x-ui`. The
|
||||
source SQLite file is left in place.
|
||||
- Docker image `ghcr.io/mhsanaei/3x-ui`; PostgreSQL profile
|
||||
`docker compose --profile postgres up -d`. Fail2ban IP-limit enforcement needs
|
||||
`NET_ADMIN` + `NET_RAW` (compose grants them; a bare `docker run` must add
|
||||
`--cap-add=NET_ADMIN --cap-add=NET_RAW`).
|
||||
- Never state that a `XUI_*` variable does not exist without grepping
|
||||
`internal/config/` and `internal/tunnelmonitor/` first. The
|
||||
`XUI_TUNNEL_HEALTH_*` family is the usual answer to "the panel restarts Xray
|
||||
every few minutes".
|
||||
- Security per inbound is none / tls / reality. XTLS is a VLESS *flow*
|
||||
(`xtls-rprx-vision`), not a security setting — never tell anyone to pick XTLS
|
||||
in the security dropdown.
|
||||
- Never hardcode a version. For "is this already fixed" use
|
||||
`gh release list -L 10`, `gh search commits`, and `git log -S`.
|
||||
@@ -8,6 +8,7 @@ on:
|
||||
- "go.sum"
|
||||
- "frontend/**"
|
||||
- ".nvmrc"
|
||||
- "Makefile"
|
||||
- ".github/workflows/ci.yml"
|
||||
push:
|
||||
branches:
|
||||
@@ -18,6 +19,7 @@ on:
|
||||
- "go.sum"
|
||||
- "frontend/**"
|
||||
- ".nvmrc"
|
||||
- "Makefile"
|
||||
- ".github/workflows/ci.yml"
|
||||
|
||||
permissions:
|
||||
@@ -188,6 +190,9 @@ jobs:
|
||||
- name: Lint
|
||||
run: npm run lint
|
||||
working-directory: frontend
|
||||
- name: Format check
|
||||
run: npm run format:check
|
||||
working-directory: frontend
|
||||
- name: Typecheck
|
||||
run: npm run typecheck
|
||||
working-directory: frontend
|
||||
|
||||
+502
-820
File diff suppressed because it is too large
Load Diff
@@ -42,6 +42,9 @@ jobs:
|
||||
- name: Lint
|
||||
run: pnpm lint
|
||||
|
||||
- name: Format check
|
||||
run: pnpm format:check
|
||||
|
||||
- name: Test
|
||||
run: pnpm test
|
||||
|
||||
|
||||
+1
-1
@@ -32,7 +32,7 @@ linters:
|
||||
# golang.org/x/tools/go/packages is a generator change, out of scope here.
|
||||
- linters:
|
||||
- staticcheck
|
||||
text: "SA1019: parser.ParseDir"
|
||||
text: 'SA1019: (go/)?parser\.ParseDir'
|
||||
# ST1005 (capitalized error strings) conflicts with intentional
|
||||
# user-facing error copy that tests assert verbatim.
|
||||
- linters:
|
||||
|
||||
@@ -8,7 +8,7 @@ index, layering rules), read `docs/architecture.md` on demand — do not guess
|
||||
file locations when it can answer in one hop.
|
||||
|
||||
## Stack
|
||||
- Backend: Go 1.26 (`module github.com/mhsanaei/3x-ui/v3`), Gin, GORM.
|
||||
- Backend: Go 1.27 (`module github.com/mhsanaei/3x-ui/v3`), Gin, GORM.
|
||||
Runs Xray-core as a managed child process (`internal/xray/process.go`) and
|
||||
imports `github.com/xtls/xray-core` for config types + gRPC stats/handler/router
|
||||
API. MTProto inbounds run a second managed child — the `mtg-multi` binary
|
||||
@@ -41,6 +41,14 @@ file locations when it can answer in one hop.
|
||||
- `internal/xray/geodata/` — streaming geosite/geoip `.dat` reader (cached
|
||||
category index + paged entries) and `geosite:`/`geoip:`/`ext:` token parsing.
|
||||
- `internal/mtproto/` — MTProto inbounds via the bundled `mtg-multi` binary.
|
||||
- `internal/amneziawg/` — AmneziaWG protocol shape: instance/peer derivation
|
||||
from an inbound, 3.1 obfuscation param generation + validation, port-forward
|
||||
spec parsing.
|
||||
- `internal/amneziawgnet/` — embedded AmneziaWG runtime: amneziawg-go device
|
||||
over a gVisor userspace netstack, per-inbound reconcile manager, TCP/UDP
|
||||
relay into a loopback per-peer-auth SOCKS5 Xray inbound, port-forward
|
||||
listeners, per-peer IPv6 egress aliases.
|
||||
- `internal/pia/` — PIA WireGuard protocol client (auth, signed server list, `/addKey`).
|
||||
- `internal/sub/` — subscription server (raw / JSON / Clash).
|
||||
- `internal/eventbus/` — in-process pub/sub (outbound/node health, xray.crash,
|
||||
cpu.high, memory.high, login.attempt).
|
||||
@@ -50,7 +58,7 @@ file locations when it can answer in one hop.
|
||||
- `controller/` — panel + REST API handlers; OpenAPI at /panel/api/openapi.json.
|
||||
- `service/` — business logic (InboundService, SettingService, XrayService,
|
||||
node sync); subpackages tgbot/, email/, outbound/, panel/, integration/.
|
||||
- `job/` — 17 cron jobs (traffic, fail2ban IP-limit, node heartbeat/sync, LDAP,
|
||||
- `job/` — 18 cron jobs (traffic, fail2ban IP-limit, node heartbeat/sync, LDAP,
|
||||
CPU/memory watchdogs, …); full table in `docs/architecture.md` §5.4.
|
||||
- `middleware/`, `entity/`, `global/`, `session/` (CSRF), `network/`,
|
||||
`runtime/` (master/sub-node over mTLS), `websocket/`.
|
||||
@@ -59,8 +67,7 @@ file locations when it can answer in one hop.
|
||||
- `tools/openapigen/` — Go generator that emits frontend types + Zod/JSON schemas
|
||||
into `frontend/src/generated/` from Go structs. The OpenAPI doc itself
|
||||
(`frontend/public/openapi.json`) is assembled from those + `endpoints.ts` by
|
||||
`frontend/scripts/build-openapi.mjs`. (`tools/seedperf/` is a separate seeding
|
||||
/load helper.)
|
||||
`frontend/scripts/build-openapi.mjs`.
|
||||
- `docs/` — separate Next.js/Fumadocs site (pnpm, own CI in `docs-ci.yml`,
|
||||
outside `make verify`). Holds a THIRD independent implementation of
|
||||
link/subscription generation in `docs/lib/xray/` — check it whenever
|
||||
|
||||
+6
-5
@@ -4,7 +4,7 @@ Thanks for taking the time to contribute to 3x-ui. This guide gets a development
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- **Go 1.26+** (the version pinned in `go.mod`)
|
||||
- **Go 1.27+** (the version pinned in `go.mod`)
|
||||
- **Node.js 24 LTS** (the version pinned in `.nvmrc`) and npm 10+ (for the React frontend)
|
||||
- **Git**
|
||||
- **A C compiler** — required by the CGo SQLite driver (`github.com/mattn/go-sqlite3`). Linux and macOS already ship one; for Windows see below.
|
||||
@@ -186,7 +186,7 @@ Only a genuinely **standalone bundle** (like `login` or `subpage`, reachable wit
|
||||
- **Function components + hooks** everywhere. No class components.
|
||||
- **Comments in committed Go/TS/TSX: 2 lines MAX per comment block**, spent on the *why* a name cannot hold — an invariant, an issue number, a non-obvious constraint. Names should carry the meaning; rename rather than annotate. Compiler and tool directives (`//go:build`, `//go:generate`, `//nolint:`) are exempt, and HTML `<!-- ... -->` is fine for template structure.
|
||||
- **Persian and Arabic users are first-class.** When writing Persian text in toasts or labels, isolate code identifiers on their own lines so RTL reading flows. (Full RTL layout is not currently wired through AntD `ConfigProvider direction` — only the Jalali date picker is RTL-aware — so treat RTL as an open area, not a solved one.)
|
||||
- **Schemas over `any`.** New config shapes go in `src/schemas/`; `@typescript-eslint/no-explicit-any` is an error and production schemas use no `.loose()`. Validate form fields with `antdRule(Schema.shape.field, t)` rather than inline `z.string()` in rules.
|
||||
- **Schemas over `any`.** New config shapes go in `src/schemas/`; oxlint's `typescript/no-explicit-any` is an error and production schemas use no `.loose()`. Validate form fields with `antdRule(Schema.shape.field, t)` rather than inline `z.string()` in rules.
|
||||
- **Document new endpoints.** Every new `g.POST`/`g.GET` in `internal/web/controller/` needs a matching entry in `src/pages/api-docs/endpoints.ts` — it drives both the in-panel API docs and the generated OpenAPI/Zod (`npm run gen:api` / `gen:zod`).
|
||||
- **Do not break link generation.** Share-link logic lives in `src/lib/xray/` (`inbound-link.ts`, `outbound-link-parser.ts`, …) and is round-tripped by the golden fixture suite — run `npm run test` after any change to URL generation, defaults, or TLS/Reality handling, and regenerate snapshots (`npx vitest run -u`) only for intentional changes. Two runtime paths consume it: the **inbounds page** and the **clients page** subscription links (`/panel/api/clients/subLinks/:subId` → backend `GetSubs`); exercise both.
|
||||
- **Vite is pinned to an exact version** (no `^`) in `frontend/package.json` — read the live version there rather than trusting a number quoted here — so local, CI, and release builds resolve identically. Bump it deliberately and verify both `npm run dev` and `npm run build` afterward.
|
||||
@@ -200,7 +200,8 @@ frontend/
|
||||
├── login.html — login + 2FA entry
|
||||
├── subpage.html — public subscription viewer entry
|
||||
├── tsconfig.json — strict, jsx: "react-jsx", paths "@/*" → "src/*"
|
||||
├── eslint.config.js — ESLint flat config (@eslint/js + typescript-eslint + react-hooks)
|
||||
├── .oxlintrc.json — oxlint config (typescript + react-hooks + jsx-a11y)
|
||||
├── tools/oxlint/ — input-number-guard.mjs (#6121/#6127 guard as a JS plugin)
|
||||
├── vite.config.js
|
||||
├── vitest.config.ts
|
||||
├── scripts/ — build-openapi.mjs (endpoints.ts → openapi.json)
|
||||
@@ -279,7 +280,7 @@ CI runs this for you nightly (and on demand) via `.github/workflows/mutation.yml
|
||||
|
||||
### CI
|
||||
|
||||
`.github/workflows/ci.yml` runs per PR: `go-test` (with `-shuffle -count=1`), a `race` job (`-race -shuffle -count=1`), a `fuzz-smoke` job on the critical parsers, and the frontend `typecheck`/`lint`/`test`/`build`/`build-storybook`. Snapshots are regression guards — regenerate them (`npx vitest run -u`) only for intentional output changes, never to make a red test green.
|
||||
`.github/workflows/ci.yml` runs per PR: `go-test` (with `-shuffle -count=1`), a `race` job (`-race -shuffle -count=1`), a `fuzz-smoke` job on the critical parsers, and the frontend `typecheck`/`lint`/`format:check`/`test`/`build`/`build-storybook`. Snapshots are regression guards — regenerate them (`npx vitest run -u`) only for intentional output changes, never to make a red test green.
|
||||
|
||||
## Sending a pull request
|
||||
|
||||
@@ -288,7 +289,7 @@ CI runs this for you nightly (and on demand) via `.github/workflows/mutation.yml
|
||||
3. Run the relevant checks before pushing:
|
||||
- `go build ./...`
|
||||
- `go test ./...` (when Go code changed)
|
||||
- `cd frontend && npm run typecheck && npm run lint && npm run test && npm run build && npm run build-storybook` (when the frontend changed; CI runs this same set on every PR via `.github/workflows/ci.yml`)
|
||||
- `cd frontend && npm run typecheck && npm run lint && npm run format:check && npm run test && npm run build && npm run build-storybook` (when the frontend changed; CI runs this same set on every PR via `.github/workflows/ci.yml`)
|
||||
4. Commit messages follow the existing pattern in `git log` — `<area>: short imperative summary`, then a body explaining the *why*. Conventional-commit prefixes (`feat`, `fix`, `refactor`, `chore`, `style`, `docs`) are encouraged.
|
||||
5. Open the PR against `main` with a brief description of what changed and how to test it.
|
||||
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ RUN npm run build
|
||||
# ========================================================
|
||||
# Stage: Builder
|
||||
# ========================================================
|
||||
FROM golang:1.26-alpine AS builder
|
||||
FROM golang:1.27-alpine AS builder
|
||||
WORKDIR /app
|
||||
ARG TARGETARCH
|
||||
|
||||
|
||||
@@ -31,12 +31,16 @@ lint-go: dist-stub ## golangci-lint on Go sources
|
||||
golangci-lint run
|
||||
|
||||
.PHONY: lint-fe
|
||||
lint-fe: ## ESLint on frontend sources
|
||||
lint-fe: ## oxlint on frontend sources
|
||||
cd $(FRONTEND) && npm run lint
|
||||
|
||||
.PHONY: lint
|
||||
lint: lint-go lint-fe ## All linters
|
||||
|
||||
.PHONY: format-check
|
||||
format-check: ## oxfmt in check mode on frontend sources
|
||||
cd $(FRONTEND) && npm run format:check
|
||||
|
||||
.PHONY: typecheck
|
||||
typecheck: ## tsc --noEmit
|
||||
cd $(FRONTEND) && npm run typecheck
|
||||
@@ -76,8 +80,8 @@ build: build-fe ## Build the frontend then the Go binary
|
||||
build-storybook: ## Build the static Storybook (compile-checks all stories)
|
||||
cd $(FRONTEND) && npm run build-storybook
|
||||
|
||||
# The PR gate. Matches ci.yml: codegen freshness, both linters, typecheck,
|
||||
# both test suites, a full build, and the Storybook compile-check.
|
||||
# The PR gate. Matches ci.yml: codegen freshness, both linters, the formatter,
|
||||
# typecheck, both test suites, a full build, and the Storybook compile-check.
|
||||
.PHONY: verify
|
||||
verify: gen-check lint typecheck msw-worker-check test build build-storybook ## Full local gate (mirrors CI)
|
||||
verify: gen-check lint format-check typecheck msw-worker-check test build build-storybook ## Full local gate (mirrors CI)
|
||||
@echo "verify: OK"
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
# Review instructions
|
||||
|
||||
3x-ui is a Go (Gin + GORM) web panel that generates configuration, share links
|
||||
and subscriptions for other programs — Xray-core, mihomo, sing-box, mtg-multi —
|
||||
and is deployed by operators who upgrade in place. Judge findings by what
|
||||
breaks for those consumers and operators, not by style.
|
||||
|
||||
## Severity
|
||||
|
||||
Mark every finding with exactly one of these, at the start of the finding:
|
||||
|
||||
| Marker | Severity | Use it for |
|
||||
| --- | --- | --- |
|
||||
| 🔴 | Important | A defect this pull request introduces or makes worse, in one of the classes under "What Important means here". Worth fixing before it merges. |
|
||||
| 🟡 | Nit | Style, naming, refactoring, and an ordinary `CLAUDE.md` violation the change introduces — a source comment block over two lines, a fix larger than the bug it removes, a test `CLAUDE.md` rejects outright. |
|
||||
| 🟣 | Pre-existing | A real bug you hit while reading that this pull request neither introduced nor made worse. |
|
||||
|
||||
Not every `CLAUDE.md` rule is a nit. The three listed below — the dispatch
|
||||
rule, the migration rule, the endpoint chain — are Important, because each one
|
||||
passes every local test and breaks a real deployment.
|
||||
|
||||
Severity follows what this pull request did, not how alarming the defect looks
|
||||
on its own. One the change worsens is 🔴 for the regression it added, not for
|
||||
the whole defect; one it merely brought into view is 🟣.
|
||||
|
||||
Checking what this panel emits means reading far more code than the diff
|
||||
changes, so pre-existing bugs surface on every review. One already on the base
|
||||
branch stays 🟣 however bad it is: this pull request did not cause it, so it
|
||||
cannot be a reason to hold this pull request. Say in one clause that it
|
||||
predates the change. The exception is a live security hole on an exposed
|
||||
surface — still 🟣, but open the summary with it.
|
||||
|
||||
## What Important means here
|
||||
|
||||
- Security on the exposed surfaces: `internal/web/controller/`, session and
|
||||
middleware code, the PUBLIC `internal/sub/` subscription server, and Xray
|
||||
config generation in `internal/xray/`.
|
||||
- A state-changing inbound or client operation that bypasses `runtime.Runtime`
|
||||
(`internal/web/runtime/`) and calls `internal/xray/api.go` directly, or
|
||||
dispatches from a controller or cron job. It passes every local test and
|
||||
silently breaks every multi-node deployment.
|
||||
- A schema or model change without a matching hand-written migration in
|
||||
`internal/database/db.go`, one that behaves differently on SQLite and
|
||||
PostgreSQL, or one that loses or overwrites operator data on upgrade or
|
||||
rollback. There are no migration files and no down-migrations.
|
||||
- A change to what the panel emits on the wire — Xray config JSON, share
|
||||
links, subscription/Clash YAML, mtg-multi TOML — that a downstream client
|
||||
would reject or read differently, or that makes the three independent link
|
||||
implementations (Go `internal/util/link/` + `internal/sub/`, TS
|
||||
`frontend/src/lib/xray/`, TS `docs/lib/xray/`) diverge from one another.
|
||||
- Any edit to `.github/workflows/`: this repository runs workflows with
|
||||
secrets against a public fork stream. Untrusted expression interpolation
|
||||
into `run:` blocks, broadened permissions, weakened guards, or a job that
|
||||
executes pull-request code.
|
||||
|
||||
## Always check
|
||||
|
||||
- A new `g.POST`/`g.GET` in `internal/web/controller/` needs the whole chain:
|
||||
an entry in `frontend/src/pages/api-docs/endpoints.ts`, regenerated
|
||||
artefacts (`make gen`), any new API-boundary struct added to `StructAllow`
|
||||
in `tools/openapigen/main.go`, and `frontend/public/openapi.json` copied to
|
||||
`docs/public/openapi.json` with the docs MDX regenerated
|
||||
(`cd docs && pnpm gen:api`). CI checks the first three; the docs copy is
|
||||
checked by nothing — a missed copy is Important, not a nit.
|
||||
- A bug fix carries a test that would fail without the fix. A test that cannot
|
||||
tell the broken behaviour from the fixed one passes before and after, so it
|
||||
certifies nothing and is itself the finding — asserting only `err != nil` or
|
||||
`len(x) > 0`, or going green by regenerating golden fixtures or Vitest
|
||||
snapshots.
|
||||
- No second way to do a thing already decided: Go tests are stdlib `testing`
|
||||
(never testify), the panel is Ant Design (never Tailwind or shadcn). Neither
|
||||
golangci-lint nor oxlint forbids the import, so it passes CI clean.
|
||||
|
||||
## Do not report
|
||||
|
||||
- Anything CI already enforces: golangci-lint and gofumpt, oxlint, format
|
||||
and typecheck, govulncheck, and `npm audit --omit=dev --audit-level=high`.
|
||||
A dev-dependency advisory is out of scope on purpose: it ships to nobody.
|
||||
- The contents of generated files (`frontend/src/generated/`,
|
||||
`frontend/public/openapi.json`, `docs/public/openapi.json`) or lock files.
|
||||
Those files being STALE after a source change is reportable; their style
|
||||
is not.
|
||||
- Missing tests for getters, constants, renames or pure map lookups —
|
||||
`CLAUDE.md` rejects such tests outright.
|
||||
- A missing or unreferenced i18n key.
|
||||
`frontend/src/test/i18n-dead-keys.test.ts` pins the 13 locale files in
|
||||
`internal/web/translation/` in both directions, so the `frontend` job is
|
||||
already red. Report the failing check, not the key.
|
||||
|
||||
## A higher bar, not silence
|
||||
|
||||
Everything named under "What Important means here" gets full scrutiny. Two
|
||||
areas do not — they earn review, but report there only what you are
|
||||
near-certain about and that actually breaks something:
|
||||
|
||||
- `docs/` — the standalone Fumadocs site, with its own CI and its own
|
||||
dependency tree. `docs/lib/xray/` is the exception and gets full scrutiny:
|
||||
it is the third link implementation.
|
||||
- `internal/web/translation/` — the key set is CI's job and the wording of a
|
||||
translation is nobody's here.
|
||||
|
||||
## Verification bar
|
||||
|
||||
- A claim about behaviour needs a `file:line` citation from this repository,
|
||||
not an inference from a name.
|
||||
- A claim that a downstream client rejects or requires a wire-format detail —
|
||||
a config key, JSON tag, URI query parameter, YAML or TOML key, an encoding
|
||||
or hash choice — must name the upstream symbol that decides it (repository,
|
||||
file, identifier). If you cannot verify it, keep the finding but say
|
||||
explicitly that it is unverified instead of asserting it.
|
||||
- "CI passed" is a claim too, and needs the same evidence: say it only of a
|
||||
run you actually read. A green one proves less here than it looks — only
|
||||
`postgres-durable-first` runs against PostgreSQL, `go-test` and `race` are
|
||||
SQLite, and `XRAY_E2E_BINARY` and `XUI_SCALE_TEST` are set by no job, so
|
||||
those tests have never run in CI at all. Where a change touches dialect,
|
||||
migration or Xray gRPC code that no job exercised, say it is unverified
|
||||
rather than repeating a green tick as proof.
|
||||
|
||||
## Cap the volume
|
||||
|
||||
🔴 findings are never capped. Report every one.
|
||||
|
||||
Report at most five 🟡 nits and at most three 🟣 pre-existing bugs. Past that,
|
||||
say "plus N similar" in the summary instead of posting them.
|
||||
|
||||
A cap decides WHICH ones survive, so choose rather than truncate: the same nit
|
||||
repeated across files is ONE finding with a count, not five slots; a nit in
|
||||
code this pull request wrote outranks one in code it only moved; and a nit
|
||||
nobody would act on does not deserve a slot at all.
|
||||
|
||||
After the first review of a pull request, report 🔴 findings only: a one-line
|
||||
fix must not reach round seven on style.
|
||||
|
||||
## What the comment must show
|
||||
|
||||
Open with a one-line tally — `2 🔴 / 4 🟡 / 1 🟣` — so the author sees the
|
||||
shape of the review before the detail. When nothing is 🔴, lead with
|
||||
`No blocking issues` and put the tally after it.
|
||||
|
||||
The posted comment is the only part of a review anyone sees, so a bare "no
|
||||
issues found" is a receipt, not a review: nothing in it says whether the diff
|
||||
was read or the run died early. Every comment therefore ends with a short
|
||||
coverage list — one line per area actually checked, naming what was examined
|
||||
and what it turned out to be, plus the head SHA and the size of the diff it
|
||||
covers. Say which claims could not be verified and why, including a check
|
||||
this environment blocked. Keep that coverage list under ten lines; it is
|
||||
evidence, not a retelling of the pull request.
|
||||
@@ -0,0 +1,198 @@
|
||||
package main
|
||||
|
||||
// The Claude bot prompts in .github/workflows/claude-bot.yml no longer restate
|
||||
// repository facts; they read .github/claude/repo-context.md instead. A stale
|
||||
// claim in that file is invisible until it produces a wrong review, so every
|
||||
// claim a machine can check is pinned here.
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
const (
|
||||
botContextPath = ".github/claude/repo-context.md"
|
||||
reviewPath = "REVIEW.md"
|
||||
ciWorkflowPath = ".github/workflows/ci.yml"
|
||||
)
|
||||
|
||||
func readRepoFile(t *testing.T, path string) string {
|
||||
t.Helper()
|
||||
b, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatalf("read %s: %v", path, err)
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
// section returns the text between two markers, so a table is matched only
|
||||
// inside the heading that owns it.
|
||||
func section(t *testing.T, doc, from, to string) string {
|
||||
t.Helper()
|
||||
i := strings.Index(doc, from)
|
||||
if i < 0 {
|
||||
t.Fatalf("%s no longer contains the heading %q", botContextPath, from)
|
||||
}
|
||||
rest := doc[i+len(from):]
|
||||
if before, _, ok := strings.Cut(rest, to); ok {
|
||||
return before
|
||||
}
|
||||
return rest
|
||||
}
|
||||
|
||||
func TestBotContextLocaleFileCount(t *testing.T) {
|
||||
doc := readRepoFile(t, botContextPath)
|
||||
m := regexp.MustCompile("`internal/web/translation/` \\((\\d+) files\\)").FindStringSubmatch(doc)
|
||||
if m == nil {
|
||||
t.Fatalf("%s no longer states the locale file count as \"`internal/web/translation/` (N files)\"", botContextPath)
|
||||
}
|
||||
files, err := filepath.Glob("internal/web/translation/*.json")
|
||||
if err != nil {
|
||||
t.Fatalf("glob locales: %v", err)
|
||||
}
|
||||
if got := len(files); m[1] != itoa(got) {
|
||||
t.Errorf("%s claims %s locale files, internal/web/translation/ holds %d; update the claim and every prompt that relies on it", botContextPath, m[1], got)
|
||||
}
|
||||
}
|
||||
|
||||
func itoa(n int) string {
|
||||
if n == 0 {
|
||||
return "0"
|
||||
}
|
||||
var b []byte
|
||||
for n > 0 {
|
||||
b = append([]byte{byte('0' + n%10)}, b...)
|
||||
n /= 10
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func TestBotContextNamesRealCIJobs(t *testing.T) {
|
||||
doc := readRepoFile(t, botContextPath)
|
||||
ci := readRepoFile(t, ciWorkflowPath)
|
||||
table := section(t, doc, "## What CI runs", "**What CI does NOT prove.**")
|
||||
rows := regexp.MustCompile("(?m)^\\| `([a-z0-9-]+)` \\|").FindAllStringSubmatch(table, -1)
|
||||
if len(rows) < 5 {
|
||||
t.Fatalf("expected the CI table in %s to list at least 5 jobs, found %d", botContextPath, len(rows))
|
||||
}
|
||||
for _, r := range rows {
|
||||
t.Run(r[1], func(t *testing.T) {
|
||||
if !strings.Contains(ci, "\n "+r[1]+":\n") {
|
||||
t.Errorf("%s describes a CI job %q that %s does not define", botContextPath, r[1], ciWorkflowPath)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBotContextNamesRealPaths(t *testing.T) {
|
||||
// REVIEW.md briefs the review job the way repo-context.md briefs the
|
||||
// issue bot, so both get their paths pinned.
|
||||
// internal/web/dist and frontend/node_modules are build output: absent from a
|
||||
// fresh clone, created by `make dist-stub` and `npm ci`.
|
||||
generated := map[string]bool{
|
||||
"internal/web/dist/": true,
|
||||
"frontend/node_modules": true,
|
||||
"frontend/src/generated/": true,
|
||||
}
|
||||
seen := map[string]bool{}
|
||||
counts := map[string]int{}
|
||||
for _, src := range []string{botContextPath, reviewPath} {
|
||||
for _, m := range regexp.MustCompile("`([^`]+)`").FindAllStringSubmatch(readRepoFile(t, src), -1) {
|
||||
p := m[1]
|
||||
if !regexp.MustCompile(`^(internal|frontend|docs|tools|\.github)/`).MatchString(p) ||
|
||||
strings.ContainsAny(p, "*{ ") || generated[p] || seen[p] {
|
||||
continue
|
||||
}
|
||||
seen[p] = true
|
||||
counts[src]++
|
||||
t.Run(p, func(t *testing.T) {
|
||||
if _, err := os.Stat(strings.TrimSuffix(p, "/")); err != nil {
|
||||
t.Errorf("%s names %q, which does not exist; the bot prompts trust this file", src, p)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
if counts[botContextPath] < 20 {
|
||||
t.Errorf("expected the bot context to name at least 20 repository paths, found %d - has the file been gutted?", counts[botContextPath])
|
||||
}
|
||||
}
|
||||
|
||||
func TestBotContextSkipGatesExist(t *testing.T) {
|
||||
doc := readRepoFile(t, botContextPath)
|
||||
table := section(t, doc, "**What CI does NOT prove.**", "Mutation testing")
|
||||
// [A-Z0-9_] and not [A-Z_]: XRAY_E2E_BINARY carries a digit, and excluding it
|
||||
// silently dropped that gate from the check instead of failing.
|
||||
gates := regexp.MustCompile("`((?:XUI|XRAY)_[A-Z0-9_]+)`").FindAllStringSubmatch(table, -1)
|
||||
if len(gates) < 5 {
|
||||
t.Fatalf("expected at least 5 skip-gate variables in %s, found %d", botContextPath, len(gates))
|
||||
}
|
||||
var sources []string
|
||||
err := filepath.WalkDir("internal", func(path string, d os.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !d.IsDir() && strings.HasSuffix(path, ".go") {
|
||||
sources = append(sources, path)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("walk internal: %v", err)
|
||||
}
|
||||
for _, g := range gates {
|
||||
t.Run(g[1], func(t *testing.T) {
|
||||
for _, f := range sources {
|
||||
if strings.Contains(readRepoFile(t, f), g[1]) {
|
||||
return
|
||||
}
|
||||
}
|
||||
t.Errorf("%s lists %s as a test skip gate, but no .go file under internal/ reads it", botContextPath, g[1])
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// REVIEW.md tells the reviewer which CI job proves what, and which skip gates
|
||||
// mean a green run proved nothing. Both go stale silently on a rename.
|
||||
func TestReviewNamesRealCIJobsAndGates(t *testing.T) {
|
||||
doc := readRepoFile(t, reviewPath)
|
||||
ci := readRepoFile(t, ciWorkflowPath)
|
||||
// Hyphenated only: a single-word job name is indistinguishable from prose.
|
||||
jobs := regexp.MustCompile("`([a-z0-9]+(?:-[a-z0-9]+)+)`").FindAllStringSubmatch(doc, -1)
|
||||
if len(jobs) < 2 {
|
||||
t.Fatalf("expected %s to name at least 2 CI jobs in backticks, found %d", reviewPath, len(jobs))
|
||||
}
|
||||
for _, j := range jobs {
|
||||
t.Run(j[1], func(t *testing.T) {
|
||||
if !strings.Contains(ci, "\n "+j[1]+":\n") {
|
||||
t.Errorf("%s names a CI job %q that %s does not define", reviewPath, j[1], ciWorkflowPath)
|
||||
}
|
||||
})
|
||||
}
|
||||
for _, g := range regexp.MustCompile("`((?:XUI|XRAY)_[A-Z0-9_]+)`").FindAllStringSubmatch(doc, -1) {
|
||||
t.Run(g[1], func(t *testing.T) {
|
||||
if strings.Contains(ci, g[1]) {
|
||||
t.Errorf("%s claims %s is never set in CI, but %s sets it", reviewPath, g[1], ciWorkflowPath)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// The i18n rule is the one REVIEW.md states as a number, so it is the one that
|
||||
// goes wrong silently when a locale is added.
|
||||
func TestReviewLocaleFileCount(t *testing.T) {
|
||||
doc := readRepoFile(t, reviewPath)
|
||||
m := regexp.MustCompile(`(\d+) locale files`).FindStringSubmatch(doc)
|
||||
if m == nil {
|
||||
t.Fatalf("%s no longer states the i18n rule as \"N locale files\"", reviewPath)
|
||||
}
|
||||
files, err := filepath.Glob("internal/web/translation/*.json")
|
||||
if err != nil {
|
||||
t.Fatalf("glob locales: %v", err)
|
||||
}
|
||||
if got := len(files); m[1] != itoa(got) {
|
||||
t.Errorf("%s tells the reviewer to expect %s locale files, internal/web/translation/ holds %d", reviewPath, m[1], got)
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,10 @@ services:
|
||||
# with iptables, which needs NET_ADMIN. Without these caps a ban is logged
|
||||
# and shown in fail2ban status but never actually applied. NET_RAW covers
|
||||
# ip6tables. If you disable Fail2ban, you can drop cap_add.
|
||||
#
|
||||
# AmneziaWG works in this image: it runs embedded in the panel process
|
||||
# (amneziawg-go over a gVisor userspace netstack), so it needs no kernel
|
||||
# module and no host tooling. Publish its UDP listen port to use it.
|
||||
cap_add:
|
||||
- NET_ADMIN
|
||||
- NET_RAW
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"$schema": "./node_modules/oxfmt/configuration_schema.json",
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all",
|
||||
"printWidth": 100,
|
||||
"tabWidth": 2,
|
||||
"ignorePatterns": [
|
||||
"node_modules",
|
||||
".next",
|
||||
".source",
|
||||
"out",
|
||||
"pnpm-lock.yaml",
|
||||
"public/openapi.json",
|
||||
// Reflowing MDX prose merges headings into paragraphs and collapses lists
|
||||
// inside JSX components (Steps/Callout). Author MDX by hand.
|
||||
"content/**/*.mdx",
|
||||
// Generated API reference pages (fumadocs-openapi output).
|
||||
"content/docs/**/reference/api"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"$schema": "./node_modules/oxlint/configuration_schema.json",
|
||||
"ignorePatterns": [
|
||||
".next/**",
|
||||
".source/**",
|
||||
"out/**",
|
||||
"node_modules/**",
|
||||
"next-env.d.ts",
|
||||
"content/docs/**/reference/api/**"
|
||||
],
|
||||
"plugins": ["typescript", "react", "nextjs", "jsx-a11y", "import"],
|
||||
"categories": {
|
||||
"correctness": "error"
|
||||
},
|
||||
"env": {
|
||||
"browser": true,
|
||||
"node": true,
|
||||
"es2022": true
|
||||
},
|
||||
"rules": {
|
||||
"no-var": "error",
|
||||
"prefer-const": "error",
|
||||
"prefer-rest-params": "error",
|
||||
"prefer-spread": "error",
|
||||
"typescript/no-explicit-any": "error",
|
||||
"typescript/no-unused-vars": "warn",
|
||||
"typescript/ban-ts-comment": "error",
|
||||
"typescript/no-empty-object-type": "error",
|
||||
"typescript/no-namespace": "error",
|
||||
"typescript/no-require-imports": "error",
|
||||
"typescript/no-this-alias": "error",
|
||||
"typescript/no-unsafe-function-type": "error",
|
||||
"typescript/no-unused-expressions": "warn",
|
||||
"typescript/no-wrapper-object-types": "error",
|
||||
"typescript/prefer-as-const": "error",
|
||||
"typescript/triple-slash-reference": "error",
|
||||
"react-hooks/rules-of-hooks": "error",
|
||||
"react-hooks/exhaustive-deps": "warn",
|
||||
"import/no-anonymous-default-export": "warn",
|
||||
"jsx-a11y/prefer-tag-over-role": "off"
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
node_modules
|
||||
.next
|
||||
.source
|
||||
out
|
||||
pnpm-lock.yaml
|
||||
public/openapi.json
|
||||
# Don't let Prettier reflow MDX prose — it merges headings into paragraphs and
|
||||
# collapses lists inside JSX components (Steps/Callout). Author MDX by hand.
|
||||
content/**/*.mdx
|
||||
content/docs/**/reference/api
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all",
|
||||
"printWidth": 100,
|
||||
"tabWidth": 2
|
||||
}
|
||||
@@ -20,12 +20,12 @@ pnpm dev # http://localhost:3000
|
||||
| `pnpm build` | Production build |
|
||||
| `pnpm start` | Serve the production build |
|
||||
| `pnpm typecheck` | Generate MDX/route types and run `tsc --noEmit` |
|
||||
| `pnpm lint` | ESLint (flat config) |
|
||||
| `pnpm format` | Format with Prettier |
|
||||
| `pnpm lint` | oxlint (`.oxlintrc.json`) |
|
||||
| `pnpm format` | Format with oxfmt (`.oxfmtrc.json`) |
|
||||
| `pnpm test` | Run unit tests (Vitest) for `lib/xray/*` pure logic |
|
||||
| `pnpm gen:api` | Generate the API reference from `public/openapi.json` |
|
||||
|
||||
Before opening a pull request, please run `pnpm typecheck`, `pnpm lint`, and
|
||||
Before opening a pull request, please run `pnpm typecheck`, `pnpm lint`, `pnpm format:check`, and
|
||||
`pnpm test` — these are the same checks that CI runs on every PR.
|
||||
|
||||
## License
|
||||
|
||||
+16
-16
@@ -63,15 +63,15 @@ ever leaves your browser**:
|
||||
|
||||
## Tech stack
|
||||
|
||||
| Layer | Technology |
|
||||
| ---------- | ---------------------------------------------------------- |
|
||||
| Framework | [Next.js 16](https://nextjs.org) (App Router) · React 19 |
|
||||
| Docs | [Fumadocs](https://fumadocs.dev) (`-ui` / `-core` / `-mdx`) |
|
||||
| Styling | [Tailwind CSS v4](https://tailwindcss.com) |
|
||||
| Search | [Orama](https://orama.com) static index |
|
||||
| Language | TypeScript (strict) |
|
||||
| Tests | [Vitest](https://vitest.dev) for the pure `lib/xray` logic |
|
||||
| Tooling | pnpm · ESLint 9 · Prettier |
|
||||
| Layer | Technology |
|
||||
| --------- | ----------------------------------------------------------- |
|
||||
| Framework | [Next.js 16](https://nextjs.org) (App Router) · React 19 |
|
||||
| Docs | [Fumadocs](https://fumadocs.dev) (`-ui` / `-core` / `-mdx`) |
|
||||
| Styling | [Tailwind CSS v4](https://tailwindcss.com) |
|
||||
| Search | [Orama](https://orama.com) static index |
|
||||
| Language | TypeScript (strict) |
|
||||
| Tests | [Vitest](https://vitest.dev) for the pure `lib/xray` logic |
|
||||
| Tooling | pnpm · oxlint · oxfmt |
|
||||
|
||||
## Quick start
|
||||
|
||||
@@ -86,13 +86,13 @@ pnpm dev # http://localhost:3000
|
||||
|
||||
Useful scripts:
|
||||
|
||||
| Script | Description |
|
||||
| ---------------- | -------------------------------------------- |
|
||||
| `pnpm dev` | Start the dev server |
|
||||
| `pnpm build` | Production build (also typechecks) |
|
||||
| `pnpm typecheck` | Generate MDX/route types and `tsc --noEmit` |
|
||||
| `pnpm lint` | Run ESLint |
|
||||
| `pnpm test` | Run unit tests (Vitest) |
|
||||
| Script | Description |
|
||||
| ---------------- | ------------------------------------------- |
|
||||
| `pnpm dev` | Start the dev server |
|
||||
| `pnpm build` | Production build (also typechecks) |
|
||||
| `pnpm typecheck` | Generate MDX/route types and `tsc --noEmit` |
|
||||
| `pnpm lint` | Run oxlint (`.oxlintrc.json`) |
|
||||
| `pnpm test` | Run unit tests (Vitest) |
|
||||
|
||||
See [`CONTRIBUTING.md`](./CONTRIBUTING.md) for the full list and project conventions.
|
||||
|
||||
|
||||
@@ -5,13 +5,8 @@ import { createFromSource } from 'fumadocs-core/search/server';
|
||||
export const revalidate = false;
|
||||
export const dynamic = 'force-static';
|
||||
|
||||
// Static search index: works under both SSR/Vercel and static export
|
||||
// (`output: 'export'`). The client loads this prebuilt index and searches
|
||||
// in-browser (see the `type: 'static'` search option in app/[lang]/layout.tsx).
|
||||
// All locales currently hold English (fallback) content, and Orama has no
|
||||
// Persian tokenizer, so map every locale to the English tokenizer. When real
|
||||
// translations land, switch ru -> 'russian', zh -> 'mandarin' (with
|
||||
// @orama/tokenizers), etc. See https://docs.orama.com/open-source/supported-languages
|
||||
// Every locale still serves English fallback content, so all map to zbsearch's
|
||||
// English tokenizer (its SUPPORTED_LANGUAGES has no Persian or Chinese anyway).
|
||||
export const { staticGET: GET } = createFromSource(source, {
|
||||
localeMap: {
|
||||
en: 'english',
|
||||
|
||||
+105
-97
@@ -29,17 +29,17 @@ token), with a process restart as the fallback on older binaries.
|
||||
|
||||
Servers and processes, all launched from `main.go`:
|
||||
|
||||
| Server / process | Package | Purpose | Default port |
|
||||
|---|---|---|---|
|
||||
| **Panel** | `internal/web` | Admin REST/WS API + serves the embedded SPA | 2053 |
|
||||
| **Subscription** | `internal/sub` | Public endpoint that hands out client configs (raw / JSON / Clash) | `subPort` setting |
|
||||
| **Xray-core** | supervised via `internal/xray` | The actual proxy engine; a child process, not Go code | `inbounds[].port` |
|
||||
| **mtg-multi** | supervised via `internal/mtproto` | MTProto proxy child process for MTProto inbounds (multi-secret) | per inbound |
|
||||
| Server / process | Package | Purpose | Default port |
|
||||
| ---------------- | --------------------------------- | ------------------------------------------------------------------ | ----------------- |
|
||||
| **Panel** | `internal/web` | Admin REST/WS API + serves the embedded SPA | 2053 |
|
||||
| **Subscription** | `internal/sub` | Public endpoint that hands out client configs (raw / JSON / Clash) | `subPort` setting |
|
||||
| **Xray-core** | supervised via `internal/xray` | The actual proxy engine; a child process, not Go code | `inbounds[].port` |
|
||||
| **mtg-multi** | supervised via `internal/mtproto` | MTProto proxy child process for MTProto inbounds (multi-secret) | per inbound |
|
||||
|
||||
Two key ideas that explain most of the complexity:
|
||||
|
||||
1. **The DB → Xray config pipeline.** Inbounds/clients live in the DB. On every change the
|
||||
backend regenerates the Xray config and applies it — preferring a *hot diff* (live gRPC
|
||||
backend regenerates the Xray config and applies it — preferring a _hot diff_ (live gRPC
|
||||
API mutation) over a full process restart. See §5.1.
|
||||
2. **The Runtime abstraction (multi-node).** A panel can manage remote "nodes" (other 3x-ui
|
||||
instances). Every state-changing inbound/client operation is dispatched through a
|
||||
@@ -51,7 +51,8 @@ Two key ideas that explain most of the complexity:
|
||||
|
||||
## 2. Tech stack
|
||||
|
||||
**Backend (Go 1.26):**
|
||||
**Backend (Go 1.27):**
|
||||
|
||||
- Web framework: **Gin** (`gin-gonic/gin`) + sessions (cookie store), gzip.
|
||||
- ORM: **GORM** with **SQLite** (default) or **PostgreSQL** (`XUI_DB_TYPE=postgres`).
|
||||
- Scheduler: **robfig/cron/v3** (seconds-precision) for all background jobs.
|
||||
@@ -61,6 +62,7 @@ Two key ideas that explain most of the complexity:
|
||||
- Misc: gorilla/websocket, gopsutil (system stats), go-qrcode, gotp (2FA TOTP).
|
||||
|
||||
**Frontend (`frontend/`):**
|
||||
|
||||
- **React 19** + **Ant Design 6** + **Vite 8** + **TypeScript**.
|
||||
- Data layer: **TanStack Query** (`@tanstack/react-query`) over the native **Fetch API**; **Zod 4** schemas.
|
||||
- Router: **react-router 8**. Charts: **uPlot** (`frontend/src/components/viz/Sparkline.tsx`). Editor: **CodeMirror 6**.
|
||||
@@ -95,7 +97,7 @@ Browser (React, fetch)
|
||||
```
|
||||
|
||||
The controller layer is thin. **Business logic lives in services.** When something is wrong
|
||||
with *behavior*, the bug is almost always in a service file, not a controller.
|
||||
with _behavior_, the bug is almost always in a service file, not a controller.
|
||||
|
||||
### 3.2 Subscription request (end-user fetching their config)
|
||||
|
||||
@@ -134,6 +136,7 @@ node heartbeat every 5s, periodic traffic resets (hourly/daily/weekly/monthly).
|
||||
│ │ └── model/ # **ALL GORM models** (model.go ~1.1k lines + siblings:
|
||||
│ │ # node_client_traffic.go, node_client_ip.go,
|
||||
│ │ # client_global_traffic.go). ⭐ Start here for data shape.
|
||||
│ ├── pia/ # PIA WireGuard protocol client (auth, signed server list, /addKey)
|
||||
│ ├── eventbus/ # In-process pub/sub (buffered channel): outbound.down|up,
|
||||
│ │ # xray.crash, node.down|up, cpu.high, memory.high, login.attempt
|
||||
│ ├── tunnelmonitor/ # Optional tunnel health probe (XUI_TUNNEL_HEALTH_* env vars):
|
||||
@@ -161,7 +164,7 @@ node heartbeat every 5s, periodic traffic resets (hourly/daily/weekly/monthly).
|
||||
│ │ │ ├── host.go # /panel/api/hosts (per-inbound subscription host overrides)
|
||||
│ │ │ ├── server.go # /panel/api/server (status, xray version, certs, logs, DB import/export)
|
||||
│ │ │ ├── setting.go # /panel/api/setting (settings + API tokens)
|
||||
│ │ │ ├── xray_setting.go # /panel/api/xray (raw Xray config editor, WARP/Nord, geodata)
|
||||
│ │ │ ├── xray_setting.go # /panel/api/xray (raw Xray config editor, WARP/Nord/PIA, geodata)
|
||||
│ │ │ ├── api.go # /panel/api gateway (token auth, envelope + CSRF wiring)
|
||||
│ │ │ ├── index.go # login/logout/csrf/2FA
|
||||
│ │ │ ├── spa.go # SPA fallback for /panel UI routes
|
||||
@@ -200,7 +203,7 @@ node heartbeat every 5s, periodic traffic resets (hourly/daily/weekly/monthly).
|
||||
│ │ │ ├── port_conflict.go # Detect inbound port collisions
|
||||
│ │ │ ├── fallback.go # Xray fallback (SNI/ALPN routing on shared port)
|
||||
│ │ │ ├── email/ # Email notification service (SMTP)
|
||||
│ │ │ ├── integration/ # External providers: warp.go (Cloudflare WARP), nord.go (NordVPN)
|
||||
│ │ │ ├── integration/ # External providers: warp.go, nord.go, pia.go
|
||||
│ │ │ ├── outbound/ # Outbound config service
|
||||
│ │ │ ├── panel/ # Cross-cutting panel services:
|
||||
│ │ │ │ ├── panel.go # panel-level helpers
|
||||
@@ -312,8 +315,8 @@ Restart is debounced via an atomic "need restart" flag (`SetToNeedRestart` /
|
||||
### 5.2 Runtime abstraction — Local vs Remote (multi-node) ⭐ most important
|
||||
|
||||
A "node" (`model.Node`) is another 3x-ui instance this panel controls. Every state-changing
|
||||
inbound/client operation goes through the `runtime.Runtime` interface so the *same service
|
||||
code* works whether the target is the local Xray or a remote node.
|
||||
inbound/client operation goes through the `runtime.Runtime` interface so the _same service
|
||||
code_ works whether the target is the local Xray or a remote node.
|
||||
|
||||
- **Interface:** `internal/web/runtime/runtime.go` — `Name`, `AddInbound`, `DelInbound`,
|
||||
`UpdateInbound`, `AddUser`, `RemoveUser`, `UpdateUser`, `DeleteUser`, `AddClient`,
|
||||
@@ -329,7 +332,7 @@ code* works whether the target is the local Xray or a remote node.
|
||||
- **Dispatch:** `manager.go` → `Manager.RuntimeFor(nodeID *int)`; `nil` nodeID → `Local`,
|
||||
otherwise a cached/lazy-loaded `Remote`. `InvalidateNode(id)` drops a cached remote client.
|
||||
|
||||
**Node identity & attribution (the hard part).** Inbounds carry a `NodeID` *and* an
|
||||
**Node identity & attribution (the hard part).** Inbounds carry a `NodeID` _and_ an
|
||||
`OriginNodeGuid`. Because inbounds can be pushed across hops, the panel attributes traffic and
|
||||
online clients back to the originating panel using **stable GUIDs** rather than local IDs.
|
||||
Relevant logic: `service/inbound_node.go` (`ReconcileNode`, `SetRemoteTraffic`, GUID merge,
|
||||
@@ -338,6 +341,7 @@ tracking). Node "dirty" flags drive an **anti-entropy reconciliation** so an off
|
||||
inbound edits converge once it reconnects.
|
||||
|
||||
**Where to look for node bugs:**
|
||||
|
||||
- Operation not reaching a node → `runtime/remote.go` + `runtime/manager.go`.
|
||||
- Wrong traffic/online attribution across hops → `service/inbound_node.go` (GUID merge paths).
|
||||
- Node shown offline / stale status → `job/node_heartbeat_job.go` + `service/node.go` (`Probe`, `UpdateHeartbeat`).
|
||||
@@ -360,28 +364,29 @@ Periodic resets: `job/periodic_traffic_reset_job.go` (keyed off `Inbound.Traffic
|
||||
|
||||
All registered in `web.go` → `startTask()`. Each is a struct with a `Run()` method in `internal/web/job/`:
|
||||
|
||||
| Schedule | Job | Purpose / condition |
|
||||
|---|---|---|
|
||||
| `@every 1s` | `check_xray_running_job` | Restart Xray if it died (2 consecutive down checks) |
|
||||
| `@every 30s` | (inline func in `startTask`) | Debounced Xray restart — consumes the "need restart" flag (§5.1) |
|
||||
| `@every 5s` | `xray_traffic_job` | Pull traffic stats from Xray (5s start delay) |
|
||||
| `@every 5s` | `node_heartbeat_job` | Probe child nodes (online/offline) |
|
||||
| `@every 5s` | `node_traffic_sync_job` | Pull + merge node traffic; push reconciliation |
|
||||
| `@every 10s` | `check_client_ip_job` | Enforce per-client IP limits |
|
||||
| `@every 10s` | `mtproto_job` | Reconcile `mtg` sidecars against enabled MTProto inbounds |
|
||||
| `@every 5m` | `outbound_subscription_job` | Refresh outbound provider configs |
|
||||
| `@every 10m` | `clear_logs_job` (`PruneXrayLogsJob`) | Truncate Xray access/error logs once either exceeds 64 MiB |
|
||||
| `@hourly` | `warp_ip_job`, `periodic_traffic_reset_job("hourly")` | WARP IP rotation; traffic resets |
|
||||
| `@daily` | `clear_logs_job`, `periodic_traffic_reset_job("daily")`, `periodic_traffic_reset_job("monthly")` | IP-limit and Xray access/error log cleanup; daily resets and due monthly resets |
|
||||
| `@weekly` | `periodic_traffic_reset_job("weekly")` | Weekly traffic resets |
|
||||
| default `@every 1m` | `ldap_sync_job` | Only if LDAP enabled; schedule configurable |
|
||||
| default `@daily` | `stats_notify_job` | Only if TG bot enabled; schedule configurable |
|
||||
| `@every 2m` | `check_hash_storage` | Only if TG bot enabled; expires bot callback hashes |
|
||||
| `@every 1m` | `check_cpu_usage` | Only if a CPU alarm is configured (TG or email); publishes `cpu.high` |
|
||||
| `@every 1m` | `check_memory_usage` | Only if a memory alarm is configured; publishes `memory.high` |
|
||||
| configurable | `free_os_memory` | Only if `sys.MemoryReleaseIntervalMinutes() > 0`; returns heap to OS |
|
||||
| Schedule | Job | Purpose / condition |
|
||||
| ------------------- | ------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------- |
|
||||
| `@every 1s` | `check_xray_running_job` | Restart Xray if it died (2 consecutive down checks) |
|
||||
| `@every 30s` | (inline func in `startTask`) | Debounced Xray restart — consumes the "need restart" flag (§5.1) |
|
||||
| `@every 5s` | `xray_traffic_job` | Pull traffic stats from Xray (5s start delay) |
|
||||
| `@every 5s` | `node_heartbeat_job` | Probe child nodes (online/offline) |
|
||||
| `@every 5s` | `node_traffic_sync_job` | Pull + merge node traffic; push reconciliation |
|
||||
| `@every 10s` | `check_client_ip_job` | Enforce per-client IP limits |
|
||||
| `@every 10s` | `mtproto_job` | Reconcile `mtg` sidecars against enabled MTProto inbounds |
|
||||
| `@every 10s` | `amneziawg_job` | Reconcile embedded AmneziaWG interfaces against enabled local inbounds |
|
||||
| `@every 5m` | `outbound_subscription_job` | Refresh outbound provider configs |
|
||||
| `@every 10m` | `clear_logs_job` (`PruneXrayLogsJob`) | Truncate Xray access/error logs once either exceeds 64 MiB |
|
||||
| `@hourly` | `warp_ip_job`, `periodic_traffic_reset_job("hourly")` | WARP IP rotation; traffic resets |
|
||||
| `@daily` | `clear_logs_job`, `periodic_traffic_reset_job("daily")`, `periodic_traffic_reset_job("monthly")` | IP-limit and Xray access/error log cleanup; daily resets and due monthly resets |
|
||||
| `@weekly` | `periodic_traffic_reset_job("weekly")` | Weekly traffic resets |
|
||||
| default `@every 1m` | `ldap_sync_job` | Only if LDAP enabled; schedule configurable |
|
||||
| default `@daily` | `stats_notify_job` | Only if TG bot enabled; schedule configurable |
|
||||
| `@every 2m` | `check_hash_storage` | Only if TG bot enabled; expires bot callback hashes |
|
||||
| `@every 1m` | `check_cpu_usage` | Only if a CPU alarm is configured (TG or email); publishes `cpu.high` |
|
||||
| `@every 1m` | `check_memory_usage` | Only if a memory alarm is configured; publishes `memory.high` |
|
||||
| configurable | `free_os_memory` | Only if `sys.MemoryReleaseIntervalMinutes() > 0`; returns heap to OS |
|
||||
|
||||
To change *when* something runs, edit `startTask()`. To change *what* it does, edit the job file.
|
||||
To change _when_ something runs, edit `startTask()`. To change _what_ it does, edit the job file.
|
||||
|
||||
### 5.5 Type generation (Go → TypeScript) ⚠️ don't hand-edit generated files
|
||||
|
||||
@@ -400,8 +405,9 @@ frontend types (`cd frontend && npm run gen`) instead of editing `src/generated/
|
||||
### 5.6 Share-link / subscription generation
|
||||
|
||||
Two distinct code paths produce client configs:
|
||||
|
||||
- **Per-client links in the panel** (the "copy link" / QR in the UI): `service/client_link.go`
|
||||
+ `util/link/outbound.go`.
|
||||
- `util/link/outbound.go`.
|
||||
- **Subscription endpoint** (what a client app polls): `internal/sub/service.go` (raw links),
|
||||
`internal/sub/json_service.go` (JSON), `internal/sub/clash_service.go` (Clash YAML).
|
||||
**`Host` rows** (`model.Host`, edited under /panel/api/hosts) override address/SNI/path/
|
||||
@@ -438,70 +444,70 @@ Xray restart.
|
||||
GORM models in `internal/database/model/` (main file `model.go` + siblings); all registered
|
||||
for AutoMigrate in `internal/database/db.go`.
|
||||
|
||||
| Model | Table role | Notable fields |
|
||||
|---|---|---|
|
||||
| `User` | Admin login | bcrypt password, `LoginEpoch` (invalidates sessions) |
|
||||
| `Inbound` | An Xray inbound | `Tag` (unique), `Port`, `Protocol`, `Settings`/`StreamSettings`/`Sniffing` (JSON), `Enable`, `TrafficReset`, `NodeID`, **`OriginNodeGuid`**, `ClientStats` (assoc) |
|
||||
| `Client` | In-memory client view | UUID/email/flow/limits (parsed from inbound JSON; not persisted) |
|
||||
| `ClientRecord` | Persisted client (`clients`) | `Email` (unique), `SubID`, `UUID`, `TotalGB`, `ExpiryTime`, `LimitIP`, `Group`, `Reset` |
|
||||
| `ClientGroup` / `ClientInbound` | Grouping + client↔inbound join | many-to-many wiring, `FlowOverride` |
|
||||
| `ClientExternalLink` | Extra links attached to a client | `Kind`, `Value`, `Remark`, `SortIndex` |
|
||||
| `Host` | Subscription host overrides (per inbound) | `Address`, `Port`, `Sni`, `Path`, `Security`, `Fingerprint`, `SortOrder`, visibility/exclusion flags |
|
||||
| `Node` | A managed child panel | `Guid`, `Address`, `Status`, `TlsVerifyMode`, `PinnedCertSha256`, `ConfigDirty`, version/heartbeat/metric fields |
|
||||
| `NodeClientTraffic` | Per-node client traffic baseline | cross-node merge (anti-double-count) |
|
||||
| `NodeClientIp` | Per-node client IP attribution | `NodeGuid`, `Email`, `Ips` |
|
||||
| `ClientGlobalTraffic` | Cross-master usage totals | `MasterGuid`, `Email`, `Up`, `Down` |
|
||||
| `xray.ClientTraffic` | Per-client counters (`client_traffics`) | `Email`, `Up`, `Down`, `Total`, `ExpiryTime`, `LastOnline` |
|
||||
| `InboundClientIps` | IP set per client email | drives IP-limit enforcement |
|
||||
| `OutboundTraffics` | Outbound counters | per outbound tag |
|
||||
| `OutboundSubscription` | External provider subs | Warp/Nord style |
|
||||
| `Setting` | Key/value panel settings | everything configurable |
|
||||
| `ApiToken` | REST API tokens | SHA-256 hash (plaintext shown once) |
|
||||
| `InboundFallback` | Fallback routing on a shared port | SNI/ALPN/path → dest |
|
||||
| `HistoryOfSeeders` | Seeder bookkeeping | prevents re-running one-off migrations |
|
||||
| Model | Table role | Notable fields |
|
||||
| ------------------------------- | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| `User` | Admin login | bcrypt password, `LoginEpoch` (invalidates sessions) |
|
||||
| `Inbound` | An Xray inbound | `Tag` (unique), `Port`, `Protocol`, `Settings`/`StreamSettings`/`Sniffing` (JSON), `Enable`, `TrafficReset`, `NodeID`, **`OriginNodeGuid`**, `ClientStats` (assoc) |
|
||||
| `Client` | In-memory client view | UUID/email/flow/limits (parsed from inbound JSON; not persisted) |
|
||||
| `ClientRecord` | Persisted client (`clients`) | `Email` (unique), `SubID`, `UUID`, `TotalGB`, `ExpiryTime`, `LimitIP`, `Group`, `Reset` |
|
||||
| `ClientGroup` / `ClientInbound` | Grouping + client↔inbound join | many-to-many wiring, `FlowOverride` |
|
||||
| `ClientExternalLink` | Extra links attached to a client | `Kind`, `Value`, `Remark`, `SortIndex` |
|
||||
| `Host` | Subscription host overrides (per inbound) | `Address`, `Port`, `Sni`, `Path`, `Security`, `Fingerprint`, `SortOrder`, visibility/exclusion flags |
|
||||
| `Node` | A managed child panel | `Guid`, `Address`, `Status`, `TlsVerifyMode`, `PinnedCertSha256`, `ConfigDirty`, version/heartbeat/metric fields |
|
||||
| `NodeClientTraffic` | Per-node client traffic baseline | cross-node merge (anti-double-count) |
|
||||
| `NodeClientIp` | Per-node client IP attribution | `NodeGuid`, `Email`, `Ips` |
|
||||
| `ClientGlobalTraffic` | Cross-master usage totals | `MasterGuid`, `Email`, `Up`, `Down` |
|
||||
| `xray.ClientTraffic` | Per-client counters (`client_traffics`) | `Email`, `Up`, `Down`, `Total`, `ExpiryTime`, `LastOnline` |
|
||||
| `InboundClientIps` | IP set per client email | drives IP-limit enforcement |
|
||||
| `OutboundTraffics` | Outbound counters | per outbound tag |
|
||||
| `OutboundSubscription` | External provider subs | Warp/Nord style |
|
||||
| `Setting` | Key/value panel settings | everything configurable |
|
||||
| `ApiToken` | REST API tokens | SHA-256 hash (plaintext shown once) |
|
||||
| `InboundFallback` | Fallback routing on a shared port | SNI/ALPN/path → dest |
|
||||
| `HistoryOfSeeders` | Seeder bookkeeping | prevents re-running one-off migrations |
|
||||
|
||||
---
|
||||
|
||||
## 7. Symptom → File index (start here when debugging)
|
||||
|
||||
| Symptom / task | Primary file(s) | Then check |
|
||||
|---|---|---|
|
||||
| Add/modify an **API endpoint** | `controller/<resource>.go` (route registration at top of each file) | corresponding `service/*.go`, `frontend/src/pages/api-docs/endpoints.ts` |
|
||||
| **Inbound** create/update/delete behavior | `service/inbound.go`, `service/inbound_clients.go` | `runtime/*`, `service/xray.go` |
|
||||
| **Client** CRUD / limits / expiry | `service/client_crud.go`, `service/client_inbound_apply.go` | model `ClientRecord`, `service/inbound_traffic.go` |
|
||||
| **Bulk** client operations slow/wrong | `service/client_bulk.go` | `service/client_paging.go` |
|
||||
| Xray **won't apply** a config change | `service/xray.go` (`RestartXray`, `tryHotApply`) | `xray/hot_diff.go`, `xray/process.go` |
|
||||
| Xray **restarts when it shouldn't** (kills connections) | `xray/hot_diff.go` (diff not classified as hot) | `service/xray.go` |
|
||||
| **Traffic** counts wrong / reset behavior | `service/inbound_traffic.go`, `job/xray_traffic_job.go` | `service/traffic_writer.go`, `job/periodic_traffic_reset_job.go` |
|
||||
| **Node** operation not propagating | `runtime/remote.go`, `runtime/manager.go` | `service/inbound_node.go` |
|
||||
| **Multi-hop / cross-node attribution** (traffic or online clients on wrong panel) | `service/inbound_node.go` (GUID merge, `synthNodeGuid`, `effectiveNodeGuid`) | `service/node.go`, model `OriginNodeGuid`/`Node.Guid` |
|
||||
| Node stuck **offline / stale** | `job/node_heartbeat_job.go`, `service/node.go` (`Probe`, `UpdateHeartbeat`) | `runtime/tls_client.go` (TLS verify) |
|
||||
| Node **TLS / mTLS** auth failures | `runtime/tls_client.go`, `service/node_mtls.go`, `service/setting_mtls.go` | `service/node.go` (`FetchCertFingerprint`) |
|
||||
| Offline node edits **not reconciling** on reconnect | `service/inbound_node.go` (`ReconcileNode`, dirty flags) | `service/node.go` (`MarkNodeDirty`/`NodeSyncState`) |
|
||||
| **Share link / QR** malformed (per protocol) | `service/client_link.go`, `util/link/outbound.go` | `frontend/src/lib/xray/`, `frontend/src/schemas/protocols/` |
|
||||
| **Subscription** output wrong (raw/JSON/Clash) | `internal/sub/service.go` | `sub/json_service.go`, `sub/clash_service.go`, sub golden tests |
|
||||
| Subscription **host overrides** not applied | `service/host.go`, `sub/host_sub.go` | model `Host`, `frontend/src/pages/hosts/` |
|
||||
| **External subscription** import/aggregation | `sub/external_subscription.go`, `sub/external_config.go` | `sub/clash_external.go` |
|
||||
| **Settings** not saving / defaults | `service/setting.go`, `controller/setting.go` | model `Setting` |
|
||||
| **Login / 2FA / sessions / CSRF** | `controller/index.go`, `service/panel/user.go`, `middleware/` | `session/` |
|
||||
| **API tokens** | `service/panel/api_token.go`, `controller/setting.go` | model `ApiToken` |
|
||||
| **Port conflict** on inbound add | `service/port_conflict.go` | `controller/inbound.go` |
|
||||
| **Fallbacks** (shared 443, SNI routing) | `service/fallback.go`, `controller/inbound.go` | model `InboundFallback` |
|
||||
| **Geo category browser** empty / won't open | `xray/geodata/` (`Store`, `reader.go`), `service/geodata.go` | `controller/xray_setting.go` (`/panel/api/xray/geodata/*`), asset dir = `config.GetBinFolderPath()` |
|
||||
| **`geosite:`/`geoip:` token** reported unknown in a routing rule | `xray/geodata/token.go`, `service/geodata.go` (`Validate`) | `frontend/src/lib/xray/geoTokens.ts`, `frontend/src/components/geodata/` |
|
||||
| **Telegram bot** commands | `service/tgbot/` | `job/stats_notify_job.go` |
|
||||
| **Email notifications** | `service/email/` | `internal/eventbus/` (consumers) |
|
||||
| **CPU / memory alerts** not firing | `job/check_cpu_usage.go`, `job/check_memory_usage.go` | `internal/eventbus/`, notifier settings in `service/setting.go` |
|
||||
| Xray auto-restart on **dead tunnel** | `internal/tunnelmonitor/` | `XUI_TUNNEL_HEALTH_*` in `internal/config/` |
|
||||
| **WARP / Nord** outbound integration | `service/integration/warp.go` / `nord.go` | `service/outbound_subscription.go` |
|
||||
| **MTProto** proxy issues | `internal/mtproto/manager.go`, `mtproto/process*.go` | `job/mtproto_job.go` |
|
||||
| **DB migration** / new column | `internal/database/db.go` (AutoMigrate list), `migrate_data.go` | `model/model.go` |
|
||||
| **Cron schedule** changes | `web.go` → `startTask()` | the specific `job/*.go` |
|
||||
| **CORS / security headers / HTTPS** | `middleware/`, `web.go` (`initRouter`, TLS setup) | `config/` (env) |
|
||||
| **Env vars / paths / DB type** | `internal/config/config.go` | `.env.example` |
|
||||
| **Frontend route / screen** | `frontend/src/pages/<area>/`, `frontend/src/routes.tsx` | `frontend/src/api/queries/` |
|
||||
| **Frontend ↔ backend type mismatch** | regenerate: `cd frontend && npm run gen` (`tools/openapigen`) | `frontend/src/generated/` |
|
||||
| **System status / CPU / metrics** | `service/server.go`, `service/xray_metrics.go`, `service/metric_history.go` | `controller/server.go`, gopsutil |
|
||||
| Symptom / task | Primary file(s) | Then check |
|
||||
| --------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- |
|
||||
| Add/modify an **API endpoint** | `controller/<resource>.go` (route registration at top of each file) | corresponding `service/*.go`, `frontend/src/pages/api-docs/endpoints.ts` |
|
||||
| **Inbound** create/update/delete behavior | `service/inbound.go`, `service/inbound_clients.go` | `runtime/*`, `service/xray.go` |
|
||||
| **Client** CRUD / limits / expiry | `service/client_crud.go`, `service/client_inbound_apply.go` | model `ClientRecord`, `service/inbound_traffic.go` |
|
||||
| **Bulk** client operations slow/wrong | `service/client_bulk.go` | `service/client_paging.go` |
|
||||
| Xray **won't apply** a config change | `service/xray.go` (`RestartXray`, `tryHotApply`) | `xray/hot_diff.go`, `xray/process.go` |
|
||||
| Xray **restarts when it shouldn't** (kills connections) | `xray/hot_diff.go` (diff not classified as hot) | `service/xray.go` |
|
||||
| **Traffic** counts wrong / reset behavior | `service/inbound_traffic.go`, `job/xray_traffic_job.go` | `service/traffic_writer.go`, `job/periodic_traffic_reset_job.go` |
|
||||
| **Node** operation not propagating | `runtime/remote.go`, `runtime/manager.go` | `service/inbound_node.go` |
|
||||
| **Multi-hop / cross-node attribution** (traffic or online clients on wrong panel) | `service/inbound_node.go` (GUID merge, `synthNodeGuid`, `effectiveNodeGuid`) | `service/node.go`, model `OriginNodeGuid`/`Node.Guid` |
|
||||
| Node stuck **offline / stale** | `job/node_heartbeat_job.go`, `service/node.go` (`Probe`, `UpdateHeartbeat`) | `runtime/tls_client.go` (TLS verify) |
|
||||
| Node **TLS / mTLS** auth failures | `runtime/tls_client.go`, `service/node_mtls.go`, `service/setting_mtls.go` | `service/node.go` (`FetchCertFingerprint`) |
|
||||
| Offline node edits **not reconciling** on reconnect | `service/inbound_node.go` (`ReconcileNode`, dirty flags) | `service/node.go` (`MarkNodeDirty`/`NodeSyncState`) |
|
||||
| **Share link / QR** malformed (per protocol) | `service/client_link.go`, `util/link/outbound.go` | `frontend/src/lib/xray/`, `frontend/src/schemas/protocols/` |
|
||||
| **Subscription** output wrong (raw/JSON/Clash) | `internal/sub/service.go` | `sub/json_service.go`, `sub/clash_service.go`, sub golden tests |
|
||||
| Subscription **host overrides** not applied | `service/host.go`, `sub/host_sub.go` | model `Host`, `frontend/src/pages/hosts/` |
|
||||
| **External subscription** import/aggregation | `sub/external_subscription.go`, `sub/external_config.go` | `sub/clash_external.go` |
|
||||
| **Settings** not saving / defaults | `service/setting.go`, `controller/setting.go` | model `Setting` |
|
||||
| **Login / 2FA / sessions / CSRF** | `controller/index.go`, `service/panel/user.go`, `middleware/` | `session/` |
|
||||
| **API tokens** | `service/panel/api_token.go`, `controller/setting.go` | model `ApiToken` |
|
||||
| **Port conflict** on inbound add | `service/port_conflict.go` | `controller/inbound.go` |
|
||||
| **Fallbacks** (shared 443, SNI routing) | `service/fallback.go`, `controller/inbound.go` | model `InboundFallback` |
|
||||
| **Geo category browser** empty / won't open | `xray/geodata/` (`Store`, `reader.go`), `service/geodata.go` | `controller/xray_setting.go` (`/panel/api/xray/geodata/*`), asset dir = `config.GetBinFolderPath()` |
|
||||
| **`geosite:`/`geoip:` token** reported unknown in a routing rule | `xray/geodata/token.go`, `service/geodata.go` (`Validate`) | `frontend/src/lib/xray/geoTokens.ts`, `frontend/src/components/geodata/` |
|
||||
| **Telegram bot** commands | `service/tgbot/` | `job/stats_notify_job.go` |
|
||||
| **Email notifications** | `service/email/` | `internal/eventbus/` (consumers) |
|
||||
| **CPU / memory alerts** not firing | `job/check_cpu_usage.go`, `job/check_memory_usage.go` | `internal/eventbus/`, notifier settings in `service/setting.go` |
|
||||
| Xray auto-restart on **dead tunnel** | `internal/tunnelmonitor/` | `XUI_TUNNEL_HEALTH_*` in `internal/config/` |
|
||||
| **WARP / Nord / PIA** outbound integration | `service/integration/warp.go` / `nord.go` / `pia.go` | `internal/pia/`, `frontend/src/pages/xray/overrides/` |
|
||||
| **MTProto** proxy issues | `internal/mtproto/manager.go`, `mtproto/process*.go` | `job/mtproto_job.go` |
|
||||
| **DB migration** / new column | `internal/database/db.go` (AutoMigrate list), `migrate_data.go` | `model/model.go` |
|
||||
| **Cron schedule** changes | `web.go` → `startTask()` | the specific `job/*.go` |
|
||||
| **CORS / security headers / HTTPS** | `middleware/`, `web.go` (`initRouter`, TLS setup) | `config/` (env) |
|
||||
| **Env vars / paths / DB type** | `internal/config/config.go` | `.env.example` |
|
||||
| **Frontend route / screen** | `frontend/src/pages/<area>/`, `frontend/src/routes.tsx` | `frontend/src/api/queries/` |
|
||||
| **Frontend ↔ backend type mismatch** | regenerate: `cd frontend && npm run gen` (`tools/openapigen`) | `frontend/src/generated/` |
|
||||
| **System status / CPU / metrics** | `service/server.go`, `service/xray_metrics.go`, `service/metric_history.go` | `controller/server.go`, gopsutil |
|
||||
|
||||
---
|
||||
|
||||
@@ -522,7 +528,7 @@ for AutoMigrate in `internal/database/db.go`.
|
||||
Regenerate instead.
|
||||
7. **Models are the contract.** Changing a model field that crosses the API boundary means:
|
||||
update `model.go` → handle migration in `db.go`/`migrate_data.go` → regenerate frontend types.
|
||||
8. **Two servers, two concerns.** Admin features go in `internal/web`; anything an *end user*
|
||||
8. **Two servers, two concerns.** Admin features go in `internal/web`; anything an _end user_
|
||||
fetches goes in `internal/sub`. Don't blur them.
|
||||
9. **Cross-cutting notifications go through `internal/eventbus/`** — publish an event instead
|
||||
of importing the Telegram/email services into producers.
|
||||
@@ -536,6 +542,7 @@ The canonical gate is the **Makefile** (mirrors CI): `make verify`. Also: `make
|
||||
frontend), `make race`, `make build`. Run `make help` for everything. Raw commands:
|
||||
|
||||
**Backend (Go):**
|
||||
|
||||
```bash
|
||||
go build ./... # compile everything
|
||||
go test ./... # run all Go tests (many *_test.go alongside sources)
|
||||
@@ -548,11 +555,12 @@ go run main.go # run the panel locally (serves embedded dis
|
||||
```
|
||||
|
||||
**Frontend (`cd frontend`, Node 24 — see `.nvmrc`):**
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run dev # Vite dev server on :5173; proxies API to Go backend on :2053 (run `go run main.go` too)
|
||||
npm run typecheck # tsc --noEmit
|
||||
npm run lint # eslint src
|
||||
npm run lint # oxlint src
|
||||
npm run test # vitest (incl. golden config-generation snapshots)
|
||||
npm run gen # regenerate src/generated/* from Go (gen:zod + gen:api)
|
||||
npm run build # gen:api + vite build → outputs to internal/web/dist (then rebuild Go binary to embed)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { create } from 'zbsearch';
|
||||
import { useDocsSearch } from 'fumadocs-core/search/client';
|
||||
import { oramaStaticClient } from 'fumadocs-core/search/client/orama-static';
|
||||
import { staticClient } from 'fumadocs-core/search/client/orama-static';
|
||||
import {
|
||||
SearchDialog,
|
||||
SearchDialogClose,
|
||||
@@ -21,17 +21,13 @@ interface SharedProps {
|
||||
onOpenChange: (open: boolean) => void;
|
||||
}
|
||||
|
||||
// The static search index is keyed by locale code (en/fa/ru/zh). Fumadocs'
|
||||
// default static dialog feeds those codes to Orama as a tokenizer language, but
|
||||
// Orama only accepts full names ("english") and throws on "en" — which silently
|
||||
// breaks search entirely. All docs content is English (other locales fall back
|
||||
// to it), so re-create the dialog — the documented escape hatch for custom search
|
||||
// setups — with an initDB that always builds an English index.
|
||||
// Fumadocs' default dialog passes the index's locale code as a tokenizer language,
|
||||
// and zbsearch throws on anything but a full name — so force "english" everywhere.
|
||||
export default function SearchDialogClient(props: SharedProps) {
|
||||
const { locale } = useI18n();
|
||||
const client = useMemo(
|
||||
() =>
|
||||
oramaStaticClient({
|
||||
staticClient({
|
||||
from: '/api/search',
|
||||
locale,
|
||||
initDB: () => create({ schema: { _: 'string' }, language: 'english' }),
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
'use client';
|
||||
|
||||
import { useId, useState } from 'react';
|
||||
import { buildCurl, buildFetchSnippet, type ApiRequestInput, type HttpMethod } from '@/lib/xray/api-client';
|
||||
import {
|
||||
buildCurl,
|
||||
buildFetchSnippet,
|
||||
type ApiRequestInput,
|
||||
type HttpMethod,
|
||||
} from '@/lib/xray/api-client';
|
||||
import { ToolFrame } from './tool-frame';
|
||||
import { TextField, SelectField } from './shared/fields';
|
||||
import { OutputBlock } from './shared/output-block';
|
||||
|
||||
@@ -38,8 +38,24 @@ const DEFAULT_BALANCERS: BalancerRow[] = [
|
||||
{ tag: 'balancer', selector: 'proxy', strategy: 'leastPing', fallbackTag: '' },
|
||||
];
|
||||
const DEFAULT_RULES: RuleRow[] = [
|
||||
{ domain: 'geosite:category-ads-all', ip: '', port: '', network: 'any', inboundTag: '', targetKind: 'outbound', targetTag: 'block' },
|
||||
{ domain: '', ip: 'geoip:private', port: '', network: 'any', inboundTag: '', targetKind: 'outbound', targetTag: 'direct' },
|
||||
{
|
||||
domain: 'geosite:category-ads-all',
|
||||
ip: '',
|
||||
port: '',
|
||||
network: 'any',
|
||||
inboundTag: '',
|
||||
targetKind: 'outbound',
|
||||
targetTag: 'block',
|
||||
},
|
||||
{
|
||||
domain: '',
|
||||
ip: 'geoip:private',
|
||||
port: '',
|
||||
network: 'any',
|
||||
inboundTag: '',
|
||||
targetKind: 'outbound',
|
||||
targetTag: 'direct',
|
||||
},
|
||||
];
|
||||
|
||||
function list(s: string): string[] {
|
||||
@@ -113,7 +129,10 @@ export function RoutingBuilder() {
|
||||
type="button"
|
||||
className={addBtn}
|
||||
onClick={() =>
|
||||
setBalancers((p) => [...p, { tag: '', selector: '', strategy: 'random', fallbackTag: '' }])
|
||||
setBalancers((p) => [
|
||||
...p,
|
||||
{ tag: '', selector: '', strategy: 'random', fallbackTag: '' },
|
||||
])
|
||||
}
|
||||
>
|
||||
Add balancer
|
||||
@@ -163,7 +182,15 @@ export function RoutingBuilder() {
|
||||
onClick={() =>
|
||||
setRules((p) => [
|
||||
...p,
|
||||
{ domain: '', ip: '', port: '', network: 'any', inboundTag: '', targetKind: 'outbound', targetTag: '' },
|
||||
{
|
||||
domain: '',
|
||||
ip: '',
|
||||
port: '',
|
||||
network: 'any',
|
||||
inboundTag: '',
|
||||
targetKind: 'outbound',
|
||||
targetTag: '',
|
||||
},
|
||||
])
|
||||
}
|
||||
>
|
||||
@@ -174,13 +201,47 @@ export function RoutingBuilder() {
|
||||
{rules.map((r, i) => (
|
||||
<div key={i} className="rounded-xl border p-3">
|
||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
||||
<TextField label="Domain (comma)" value={r.domain} onChange={(v) => patchRule(i, { domain: v })} placeholder="geosite:google, example.com" />
|
||||
<TextField label="IP (comma)" value={r.ip} onChange={(v) => patchRule(i, { ip: v })} placeholder="geoip:cn, 1.1.1.1" />
|
||||
<TextField label="Port" value={r.port} onChange={(v) => patchRule(i, { port: v })} placeholder="443 or 1000-2000" />
|
||||
<SelectField label="Network" value={r.network} onChange={(v) => patchRule(i, { network: v })} options={NETWORKS} />
|
||||
<TextField label="Inbound tag (comma)" value={r.inboundTag} onChange={(v) => patchRule(i, { inboundTag: v })} placeholder="optional" />
|
||||
<SelectField label="Target kind" value={r.targetKind} onChange={(v) => patchRule(i, { targetKind: v as 'outbound' | 'balancer' })} options={TARGET_KINDS} />
|
||||
<TextField label="Target tag" value={r.targetTag} onChange={(v) => patchRule(i, { targetTag: v })} />
|
||||
<TextField
|
||||
label="Domain (comma)"
|
||||
value={r.domain}
|
||||
onChange={(v) => patchRule(i, { domain: v })}
|
||||
placeholder="geosite:google, example.com"
|
||||
/>
|
||||
<TextField
|
||||
label="IP (comma)"
|
||||
value={r.ip}
|
||||
onChange={(v) => patchRule(i, { ip: v })}
|
||||
placeholder="geoip:cn, 1.1.1.1"
|
||||
/>
|
||||
<TextField
|
||||
label="Port"
|
||||
value={r.port}
|
||||
onChange={(v) => patchRule(i, { port: v })}
|
||||
placeholder="443 or 1000-2000"
|
||||
/>
|
||||
<SelectField
|
||||
label="Network"
|
||||
value={r.network}
|
||||
onChange={(v) => patchRule(i, { network: v })}
|
||||
options={NETWORKS}
|
||||
/>
|
||||
<TextField
|
||||
label="Inbound tag (comma)"
|
||||
value={r.inboundTag}
|
||||
onChange={(v) => patchRule(i, { inboundTag: v })}
|
||||
placeholder="optional"
|
||||
/>
|
||||
<SelectField
|
||||
label="Target kind"
|
||||
value={r.targetKind}
|
||||
onChange={(v) => patchRule(i, { targetKind: v as 'outbound' | 'balancer' })}
|
||||
options={TARGET_KINDS}
|
||||
/>
|
||||
<TextField
|
||||
label="Target tag"
|
||||
value={r.targetTag}
|
||||
onChange={(v) => patchRule(i, { targetTag: v })}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-2 flex justify-end">
|
||||
<button
|
||||
|
||||
@@ -79,7 +79,15 @@ export function SubscriptionBuilder() {
|
||||
setClients((prev) => prev.map((c, j) => (i === j ? { ...c, ...p } : c)));
|
||||
}
|
||||
|
||||
const urlInput: SubUrlInput = { scheme, host, port: Number(port), subPath, jsonPath, subId, behindProxy };
|
||||
const urlInput: SubUrlInput = {
|
||||
scheme,
|
||||
host,
|
||||
port: Number(port),
|
||||
subPath,
|
||||
jsonPath,
|
||||
subId,
|
||||
behindProxy,
|
||||
};
|
||||
const urls = buildSubscriptionUrls(urlInput);
|
||||
const subClients = clients.filter((c) => c.address.trim()).map(toClient);
|
||||
|
||||
@@ -159,16 +167,33 @@ export function SubscriptionBuilder() {
|
||||
onChange={(v) => patch(i, { protocol: v as ClientProtocol })}
|
||||
options={PROTOCOLS}
|
||||
/>
|
||||
<TextField label="Remark" value={c.remark} onChange={(v) => patch(i, { remark: v })} />
|
||||
<TextField label="Address" value={c.address} onChange={(v) => patch(i, { address: v })} />
|
||||
<TextField label="Port" value={c.port} onChange={(v) => patch(i, { port: v })} inputMode="numeric" />
|
||||
<TextField
|
||||
label="Remark"
|
||||
value={c.remark}
|
||||
onChange={(v) => patch(i, { remark: v })}
|
||||
/>
|
||||
<TextField
|
||||
label="Address"
|
||||
value={c.address}
|
||||
onChange={(v) => patch(i, { address: v })}
|
||||
/>
|
||||
<TextField
|
||||
label="Port"
|
||||
value={c.port}
|
||||
onChange={(v) => patch(i, { port: v })}
|
||||
inputMode="numeric"
|
||||
/>
|
||||
<TextField
|
||||
label={c.protocol === 'vless' || c.protocol === 'vmess' ? 'UUID (id)' : 'Password'}
|
||||
value={c.credential}
|
||||
onChange={(v) => patch(i, { credential: v })}
|
||||
/>
|
||||
{c.protocol === 'ss' ? (
|
||||
<TextField label="Method" value={c.method} onChange={(v) => patch(i, { method: v })} />
|
||||
<TextField
|
||||
label="Method"
|
||||
value={c.method}
|
||||
onChange={(v) => patch(i, { method: v })}
|
||||
/>
|
||||
) : null}
|
||||
<SelectField
|
||||
label="Transport"
|
||||
@@ -200,9 +225,15 @@ export function SubscriptionBuilder() {
|
||||
</div>
|
||||
|
||||
<div className="mt-4 grid grid-cols-1 gap-4">
|
||||
<OutputBlock label="Subscription links (decoded body)" value={buildShareLinks(subClients).join('\n')} />
|
||||
<OutputBlock
|
||||
label="Subscription links (decoded body)"
|
||||
value={buildShareLinks(subClients).join('\n')}
|
||||
/>
|
||||
<OutputBlock label="Base64 body" value={buildBase64Subscription(subClients)} />
|
||||
<OutputBlock label="JSON subscription (preview)" value={buildJsonSubscription(subClients)} />
|
||||
<OutputBlock
|
||||
label="JSON subscription (preview)"
|
||||
value={buildJsonSubscription(subClients)}
|
||||
/>
|
||||
</div>
|
||||
</ToolFrame>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
---
|
||||
title: AmneziaWG
|
||||
description: Set up an AmneziaWG inbound in 3x-ui — obfuscation parameters, native IPv6, per-client port-forwarding, and routing client traffic through Xray.
|
||||
icon: Lock
|
||||
---
|
||||
|
||||
**AmneziaWG** is a WireGuard fork that adds traffic obfuscation (junk packets,
|
||||
randomized padding, and rewritten protocol magic values) so the tunnel doesn't
|
||||
look like WireGuard to deep-packet inspection. It's a popular choice where
|
||||
plain WireGuard is blocked but a WireGuard-shaped tunnel with a different
|
||||
fingerprint gets through.
|
||||
|
||||
<Callout type="info">
|
||||
AmneziaWG runs **embedded in the panel process** — `amneziawg-go` over a
|
||||
userspace (gVisor) network stack, not a kernel module. There is no DKMS
|
||||
build, no Secure Boot conflict, and no host network/kernel access
|
||||
requirement, so it works the same way inside a container as on bare
|
||||
metal. Each peer's decapsulated traffic relays into its own loopback Xray
|
||||
SOCKS5 inbound, so a peer's routing, sniffing, and per-client stats all
|
||||
come from Xray's own machinery — the same as any other protocol's
|
||||
inbound, not a separate code path.
|
||||
</Callout>
|
||||
|
||||
## Key settings
|
||||
|
||||
### Server / interface
|
||||
|
||||
| Field | What it is |
|
||||
| ------------------------ | ------------------------------------------------------------------------ |
|
||||
| **Subnet** | The tunnel's IPv4 subnet (e.g. `10.8.1.0/24`); each client gets an address from it. |
|
||||
| **MTU** | Interface MTU. Leave at the default unless you have a reason to change it. |
|
||||
| **DNS (primary/secondary)** | Seeded into downloadable client configs; the server's own interface doesn't need one. |
|
||||
| **External interface** | The host NIC a peer's IPv6 address gets aliased onto when IPv6 is enabled (see below). Leave blank to auto-detect. |
|
||||
|
||||
### Obfuscation (AmneziaWG 3.1)
|
||||
|
||||
The same values must match on both ends of the tunnel, so the server stores
|
||||
them once and every client config inherits them. The panel generates a
|
||||
randomized set for you (with a **regenerate** button) — a static, reused
|
||||
value defeats the point, since DPI can fingerprint it over time.
|
||||
|
||||
| Field | What it is |
|
||||
| ------------ | ---------------------------------------------------------------------------- |
|
||||
| **Jc** | Number of junk packets sent before the handshake. |
|
||||
| **Jmin/Jmax** | Size range (bytes) for those junk packets. `Jmin` must not exceed `Jmax`. |
|
||||
| **S1/S2** | Padding added to the handshake init/response packets. `S1 + 56` must not equal `S2` — amneziawg-go rejects a value that would make both packets the same size. |
|
||||
| **S3** | Cookie-reply padding, `0`-`64`. |
|
||||
| **S4** | Transport (data) packet padding, `0`-`32`. |
|
||||
| **H1-H4** | Magic header values that replace WireGuard's standard message-type bytes. Each is a single integer or a `low-high` range; `1`-`4` are reserved (real WireGuard message types) and must not be used. |
|
||||
| **I1-I5** | Optional signature packets — random bytes prepended before the handshake, e.g. `<r 148>`. Generated sets fill `I1` only, matching Amnezia's own generator. |
|
||||
| **HeaderProtectionKey** | A base64 32-byte key for the 3.0 header-protection mechanism. Must match on every client config; blank disables it. |
|
||||
| **ContentPaddingAddition** | A single integer or `low-high` byte range of extra padding on content packets. Kept `<= 64` by the generator so a 1420-MTU tunnel doesn't fragment. |
|
||||
| **RekeyAfterTime / RekeyTimeout / RejectAfterTime / KeepaliveTimeout / MaxHandshakeAttempts** | Handshake-timing randomization: each is a `low-high` range (seconds; attempts for the last one) the peer samples from, so session timing stops being a WireGuard fingerprint. Every `RekeyAfterTime` value must stay below every `RejectAfterTime` value. Blank keeps the WireGuard default. |
|
||||
| **RandomTrailers** | Appends a random number of bytes to the end of every packet. |
|
||||
| **DisableCookies** | Never send cookie replies — removes a DPI-visible WireGuard message type, at the cost of WireGuard's handshake-flood mitigation. |
|
||||
|
||||
<Callout type="info">
|
||||
If you enter obfuscation values by hand instead of using the generated
|
||||
defaults, keep `H1`-`H4` **non-overlapping** and above `4`, and double-check
|
||||
`S1 + 56 != S2` — a bad value here keeps the embedded interface from
|
||||
coming up at all.
|
||||
</Callout>
|
||||
|
||||
<Callout type="warn">
|
||||
The 3.1 parameters need a **3.1-capable client**. Clients must run a
|
||||
3.1-capable Amnezia app; blanking the 3.1 fields renders a config older
|
||||
clients still understand. There is no host-side version requirement —
|
||||
the panel ships its own pinned `amneziawg-go`, not whatever happens to be
|
||||
installed on the system.
|
||||
</Callout>
|
||||
|
||||
## Set it up in the panel
|
||||
|
||||
<Steps>
|
||||
|
||||
<Step>
|
||||
### Add an inbound
|
||||
|
||||
Add a new inbound, choose protocol **AmneziaWG**, and set the port and tunnel
|
||||
subnet.
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
### Leave obfuscation on defaults (or regenerate)
|
||||
|
||||
The panel fills in a randomized, kernel-valid obfuscation set automatically.
|
||||
Use **Regenerate** if you want a fresh one; there's no need to hand-edit these
|
||||
unless you have a specific reason to.
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
### Add a client
|
||||
|
||||
Each client gets its own keypair and tunnel address. Download the client's
|
||||
`.conf` or copy its share link (`vpn://…`, importable by the official
|
||||
AmneziaWG/AmneziaVPN apps) from the client list.
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
### Optional: enable IPv6
|
||||
|
||||
Turning on IPv6 allocates an IPv6 address alongside each client's IPv4 one
|
||||
from the configured IPv6 subnet. The panel aliases that address onto the
|
||||
external interface's host NIC so outbound connections carry the peer's own
|
||||
distinct public IPv6 identity — no NAT66 needed.
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
### Optional: forward ports to a client
|
||||
|
||||
Set a client's forwarded ports (e.g. `80, 443, 8000-8100`) to open a real
|
||||
listener on the host that relays that traffic straight to the client's
|
||||
tunnel address — useful for a client that needs to expose a service through
|
||||
the server.
|
||||
</Step>
|
||||
|
||||
</Steps>
|
||||
|
||||
Every AmneziaWG inbound's traffic already goes through Xray — each peer
|
||||
relays into its own loopback SOCKS5 inbound, tagged with the AmneziaWG
|
||||
inbound's own tag, so it shows up as a normal source on the
|
||||
[Routing](/docs/operations/outbounds-routing) page like any other protocol's
|
||||
inbound. There is no separate toggle for this: unlike a kernel tunnel,
|
||||
there's no other way for a peer's traffic to reach the internet once it's
|
||||
decapsulated.
|
||||
|
||||
## What the configuration looks like
|
||||
|
||||
A client's downloadable `.conf` (also what the `vpn://` share link encodes,
|
||||
base64url'd) looks like this:
|
||||
|
||||
```ini title="client .conf"
|
||||
[Interface]
|
||||
PrivateKey = <client private key>
|
||||
Address = 10.8.1.2/32
|
||||
DNS = 8.8.8.8, 8.8.4.4
|
||||
Jc = 4
|
||||
Jmin = 65
|
||||
Jmax = 220
|
||||
S1 = 87
|
||||
S2 = 44
|
||||
S3 = 21
|
||||
S4 = 9
|
||||
H1 = 462980921-463150218
|
||||
H2 = 1177681572-1177787900
|
||||
H3 = 1907413509-1907903969
|
||||
H4 = 2029908558-2030313135
|
||||
I1 = <r 148>
|
||||
HeaderProtectionKey = 8Iu83eHDA3fMKKSGaEsVW9Ycd2lYYzc0MYlk1jJTvE4=
|
||||
ContentPaddingAddition = 17-49
|
||||
RekeyAfterTime = 111-139
|
||||
RekeyTimeout = 4-7
|
||||
RejectAfterTime = 187-251
|
||||
KeepaliveTimeout = 9-14
|
||||
MaxHandshakeAttempts = 19-36
|
||||
RandomTrailers = on
|
||||
DisableCookies = on
|
||||
|
||||
# my-client
|
||||
[Peer]
|
||||
PublicKey = <server public key>
|
||||
AllowedIPs = 0.0.0.0/0, ::/0
|
||||
Endpoint = your-server:443
|
||||
PersistentKeepalive = 25
|
||||
```
|
||||
|
||||
## Not yet covered
|
||||
|
||||
<Callout type="info">
|
||||
|
||||
- **Multi-node (sub-nodes)** and **Telegram bot** — AmneziaWG inbounds haven't
|
||||
been exercised through those paths yet. They likely work (the reconciler
|
||||
runs the same way regardless of how the panel itself is deployed), but
|
||||
that's not the same as a confirmed, tested claim — treat it as unverified
|
||||
rather than assume it either way until someone reports back.
|
||||
|
||||
</Callout>
|
||||
@@ -58,6 +58,7 @@ The inbound editor accepts these protocols:
|
||||
| **Trojan** | TLS-based; supports XTLS and fallbacks. |
|
||||
| **Shadowsocks** | Includes Shadowsocks-2022 (`2022-blake3-*`) ciphers. |
|
||||
| **WireGuard** | Modern tunnel. |
|
||||
| **AmneziaWG** | Obfuscated WireGuard fork, embedded in the panel process. See [AmneziaWG](/docs/config/amneziawg). |
|
||||
| **Hysteria2** | Selected as `hysteria`; the panel emits `hysteria2://` links. |
|
||||
| **HTTP** | HTTP proxy. |
|
||||
| **Mixed (SOCKS/HTTP)** | A combined SOCKS + HTTP listener. |
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"ssl-certificates",
|
||||
"inbounds",
|
||||
"reality",
|
||||
"amneziawg",
|
||||
"transports",
|
||||
"clients",
|
||||
"subscription",
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
---
|
||||
title: Outbounds & Routing
|
||||
description: Shape egress in 3x-ui — WARP and NordVPN outbounds, outbound subscriptions (server pools), routing rules, and load balancers.
|
||||
description: Shape egress in 3x-ui — WARP, NordVPN, PIA WireGuard, outbound subscriptions, routing rules, and load balancers.
|
||||
icon: Route
|
||||
---
|
||||
|
||||
Inbounds accept clients; **outbounds** decide where their traffic goes next.
|
||||
3x-ui can route traffic through Cloudflare WARP, NordVPN, or arbitrary outbound
|
||||
pools imported from a subscription, and select between them with routing rules
|
||||
and balancers.
|
||||
3x-ui can route traffic through Cloudflare WARP, NordVPN, Private Internet Access
|
||||
(WireGuard), or arbitrary outbound pools imported from a subscription,
|
||||
and select between them with routing rules and balancers.
|
||||
|
||||
## Editing outbounds & routing
|
||||
|
||||
@@ -86,6 +86,23 @@ with a routing rule.
|
||||
accept a private key directly) and list countries/servers, so you can build a
|
||||
NordVPN outbound.
|
||||
|
||||
## PIA WireGuard
|
||||
|
||||
3x-ui can sign in with a PIA username and password, list countries/regions/servers
|
||||
from the signed PIA server list, and build a WireGuard outbound. Open
|
||||
**Xray → Outbounds → More → PIA**, sign in, pick a server, and add the outbound.
|
||||
You can add several servers (one outbound per hostname). The tag is
|
||||
`pia-<region>-<server>` (for example `pia-us-east-useast1`). Adding or using
|
||||
**Reset** on a row registers a WireGuard key with PIA `/addKey` for that server.
|
||||
The same hostname cannot be added twice. Logout clears the stored token only;
|
||||
delete unused PIA outbounds from the Outbounds list. Reset and delete do not
|
||||
revoke the WireGuard peer on the PIA account.
|
||||
|
||||
The password is not stored. The PIA API token is stored with the same
|
||||
`NODE_TOKEN_ENCRYPTION` setting as node API tokens. If you retire an old
|
||||
`XUI_NODE_TOKEN_KEY` without signing into PIA again, Add/Reset fail until you
|
||||
re-login. Peer `allowedIPs` is IPv4-only (`0.0.0.0/0`).
|
||||
|
||||
## Outbound subscriptions (server pools)
|
||||
|
||||
An **outbound subscription** imports a remote share-link subscription and injects
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
---
|
||||
title: API Tokens
|
||||
description: 'Manage Bearer tokens used for programmatic auth (bots, central
|
||||
panels acting on this node, CI). Each token has a unique name and an enabled
|
||||
flag — disable to revoke without deleting, delete to revoke permanently.
|
||||
Tokens are stored as SHA-256 hashes and the plaintext is returned only once,
|
||||
in the create response — it cannot be retrieved afterwards, so copy it then.
|
||||
Send one as <code>Authorization: Bearer <token></code> on any
|
||||
/panel/api/* request — the token is a full-admin credential.'
|
||||
description: Manage scoped Bearer tokens for programmatic auth. Tokens grant
|
||||
admin, monitor, or node-sync access, may expire, and are stored as SHA-256
|
||||
hashes. The plaintext is returned only once at creation.
|
||||
full: true
|
||||
_openapi:
|
||||
preload:
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
---
|
||||
title: Authentication
|
||||
description: >-
|
||||
Two authentication modes are supported. UI sessions use a cookie set by the
|
||||
login endpoint. Programmatic clients (bots, scripts, remote panels)
|
||||
description: Two authentication modes are supported. UI sessions use a cookie
|
||||
set by the login endpoint. Programmatic clients (bots, scripts, remote panels)
|
||||
authenticate with a Bearer token taken from Settings → Security → API Token.
|
||||
Both work for every endpoint under /panel/api/*.
|
||||
full: true
|
||||
@@ -11,51 +10,38 @@ _openapi:
|
||||
- ./public/openapi.json
|
||||
toc:
|
||||
- depth: 2
|
||||
title: >-
|
||||
Authenticate with username + password and receive a session cookie.
|
||||
title: Authenticate with username + password and receive a session cookie.
|
||||
Required before any cookie-based API call.
|
||||
url: >-
|
||||
#authenticate-with-username--password-and-receive-a-session-cookie-required-before-any-cookie-based-api-call
|
||||
url: '#authenticate-with-username--password-and-receive-a-session-cookie-required-before-any-cookie-based-api-call'
|
||||
- depth: 2
|
||||
title: Clear the session cookie. Requires the CSRF header for browser sessions.
|
||||
url: '#clear-the-session-cookie-requires-the-csrf-header-for-browser-sessions'
|
||||
- depth: 2
|
||||
title: >-
|
||||
Mint a CSRF token for the current session. The SPA replays it in the
|
||||
title: Mint a CSRF token for the current session. The SPA replays it in the
|
||||
X-CSRF-Token header on unsafe requests. Bearer-token callers can skip
|
||||
this — the middleware short-circuits CSRF for authenticated API
|
||||
requests.
|
||||
url: >-
|
||||
#mint-a-csrf-token-for-the-current-session-the-spa-replays-it-in-the-x-csrf-token-header-on-unsafe-requests-bearer-token-callers-can-skip-this--the-middleware-short-circuits-csrf-for-authenticated-api-requests
|
||||
url: '#mint-a-csrf-token-for-the-current-session-the-spa-replays-it-in-the-x-csrf-token-header-on-unsafe-requests-bearer-token-callers-can-skip-this--the-middleware-short-circuits-csrf-for-authenticated-api-requests'
|
||||
- depth: 2
|
||||
title: >-
|
||||
Returns whether 2FA is enabled on the panel — used by the login page to
|
||||
title: Returns whether 2FA is enabled on the panel — used by the login page to
|
||||
decide whether to show the OTP field.
|
||||
url: >-
|
||||
#returns-whether-2fa-is-enabled-on-the-panel--used-by-the-login-page-to-decide-whether-to-show-the-otp-field
|
||||
url: '#returns-whether-2fa-is-enabled-on-the-panel--used-by-the-login-page-to-decide-whether-to-show-the-otp-field'
|
||||
structuredData:
|
||||
headings:
|
||||
- content: >-
|
||||
Authenticate with username + password and receive a session cookie.
|
||||
- content: Authenticate with username + password and receive a session cookie.
|
||||
Required before any cookie-based API call.
|
||||
id: >-
|
||||
authenticate-with-username--password-and-receive-a-session-cookie-required-before-any-cookie-based-api-call
|
||||
- content: >-
|
||||
Clear the session cookie. Requires the CSRF header for browser
|
||||
id: authenticate-with-username--password-and-receive-a-session-cookie-required-before-any-cookie-based-api-call
|
||||
- content: Clear the session cookie. Requires the CSRF header for browser
|
||||
sessions.
|
||||
id: clear-the-session-cookie-requires-the-csrf-header-for-browser-sessions
|
||||
- content: >-
|
||||
Mint a CSRF token for the current session. The SPA replays it in the
|
||||
- content: Mint a CSRF token for the current session. The SPA replays it in the
|
||||
X-CSRF-Token header on unsafe requests. Bearer-token callers can skip
|
||||
this — the middleware short-circuits CSRF for authenticated API
|
||||
requests.
|
||||
id: >-
|
||||
mint-a-csrf-token-for-the-current-session-the-spa-replays-it-in-the-x-csrf-token-header-on-unsafe-requests-bearer-token-callers-can-skip-this--the-middleware-short-circuits-csrf-for-authenticated-api-requests
|
||||
- content: >-
|
||||
Returns whether 2FA is enabled on the panel — used by the login page
|
||||
to decide whether to show the OTP field.
|
||||
id: >-
|
||||
returns-whether-2fa-is-enabled-on-the-panel--used-by-the-login-page-to-decide-whether-to-show-the-otp-field
|
||||
id: mint-a-csrf-token-for-the-current-session-the-spa-replays-it-in-the-x-csrf-token-header-on-unsafe-requests-bearer-token-callers-can-skip-this--the-middleware-short-circuits-csrf-for-authenticated-api-requests
|
||||
- content: Returns whether 2FA is enabled on the panel — used by the login page to
|
||||
decide whether to show the OTP field.
|
||||
id: returns-whether-2fa-is-enabled-on-the-panel--used-by-the-login-page-to-decide-whether-to-show-the-otp-field
|
||||
contents: []
|
||||
---
|
||||
|
||||
|
||||
@@ -7,18 +7,14 @@ _openapi:
|
||||
- ./public/openapi.json
|
||||
toc:
|
||||
- depth: 2
|
||||
title: >-
|
||||
Send a fresh DB backup to every Telegram chat configured as an admin
|
||||
title: Send a fresh DB backup to every Telegram chat configured as an admin
|
||||
recipient. No body, no params.
|
||||
url: >-
|
||||
#send-a-fresh-db-backup-to-every-telegram-chat-configured-as-an-admin-recipient-no-body-no-params
|
||||
url: '#send-a-fresh-db-backup-to-every-telegram-chat-configured-as-an-admin-recipient-no-body-no-params'
|
||||
structuredData:
|
||||
headings:
|
||||
- content: >-
|
||||
Send a fresh DB backup to every Telegram chat configured as an admin
|
||||
- content: Send a fresh DB backup to every Telegram chat configured as an admin
|
||||
recipient. No body, no params.
|
||||
id: >-
|
||||
send-a-fresh-db-backup-to-every-telegram-chat-configured-as-an-admin-recipient-no-body-no-params
|
||||
id: send-a-fresh-db-backup-to-every-telegram-chat-configured-as-an-admin-recipient-no-body-no-params
|
||||
contents: []
|
||||
---
|
||||
|
||||
|
||||
@@ -1,195 +1,159 @@
|
||||
---
|
||||
title: Clients
|
||||
description: >-
|
||||
Manage clients as first-class entities that can be attached to one or more
|
||||
inbounds. A single client row drives the settings.clients entry in every
|
||||
inbound it belongs to. Endpoints live under /panel/api/clients.
|
||||
description: Manage clients as first-class entities that can be attached to one
|
||||
or more inbounds. A single client row drives the settings.clients entry in
|
||||
every inbound it belongs to. Endpoints live under /panel/api/clients.
|
||||
full: true
|
||||
_openapi:
|
||||
preload:
|
||||
- ./public/openapi.json
|
||||
toc:
|
||||
- depth: 2
|
||||
title: >-
|
||||
List every client with its attached inbound IDs and traffic record. The
|
||||
title: List every client with its attached inbound IDs and traffic record. The
|
||||
reverse field, if set, is returned as a nested JSON object (legacy
|
||||
JSON-encoded-string form is still accepted on write).
|
||||
url: >-
|
||||
#list-every-client-with-its-attached-inbound-ids-and-traffic-record-the-reverse-field-if-set-is-returned-as-a-nested-json-object-legacy-json-encoded-string-form-is-still-accepted-on-write
|
||||
url: '#list-every-client-with-its-attached-inbound-ids-and-traffic-record-the-reverse-field-if-set-is-returned-as-a-nested-json-object-legacy-json-encoded-string-form-is-still-accepted-on-write'
|
||||
- depth: 2
|
||||
title: >-
|
||||
Filter, sort, and paginate clients on the server. Each item is a slim
|
||||
title: 'Filter, sort, and paginate clients on the server. Each item is a slim
|
||||
row (no uuid/password/auth/flow/security/reverse/tgId) so the clients
|
||||
page can ship 25-ish rows in a few KB instead of the full table. The
|
||||
response also includes a summary computed across the full DB row set so
|
||||
dashboard counters stay stable as the user paginates or filters. Page
|
||||
size capped at 200; fetch /get/:email to obtain the full per-client
|
||||
payload for an edit/info modal.
|
||||
url: >-
|
||||
#filter-sort-and-paginate-clients-on-the-server-each-item-is-a-slim-row-no-uuidpasswordauthflowsecurityreversetgid-so-the-clients-page-can-ship-25-ish-rows-in-a-few-kb-instead-of-the-full-table-the-response-also-includes-a-summary-computed-across-the-full-db-row-set-so-dashboard-counters-stay-stable-as-the-user-paginates-or-filters-page-size-capped-at-200-fetch-getemail-to-obtain-the-full-per-client-payload-for-an-editinfo-modal
|
||||
dashboard counters stay stable as the user paginates or filters: the
|
||||
*Count fields are exact, while the email arrays beside them stop at 200
|
||||
entries so the payload does not grow with the panel. Page size capped at
|
||||
200; fetch /get/:email to obtain the full per-client payload for an
|
||||
edit/info modal.'
|
||||
url: '#filter-sort-and-paginate-clients-on-the-server-each-item-is-a-slim-row-no-uuidpasswordauthflowsecurityreversetgid-so-the-clients-page-can-ship-25-ish-rows-in-a-few-kb-instead-of-the-full-table-the-response-also-includes-a-summary-computed-across-the-full-db-row-set-so-dashboard-counters-stay-stable-as-the-user-paginates-or-filters-the-count-fields-are-exact-while-the-email-arrays-beside-them-stop-at-200-entries-so-the-payload-does-not-grow-with-the-panel-page-size-capped-at-200-fetch-getemail-to-obtain-the-full-per-client-payload-for-an-editinfo-modal'
|
||||
- depth: 2
|
||||
title: >-
|
||||
Fetch one client by email, including the inbound IDs and external config
|
||||
title: Fetch one client by email, including the inbound IDs and external config
|
||||
IDs it is attached to.
|
||||
url: >-
|
||||
#fetch-one-client-by-email-including-the-inbound-ids-and-external-config-ids-it-is-attached-to
|
||||
url: '#fetch-one-client-by-email-including-the-inbound-ids-and-external-config-ids-it-is-attached-to'
|
||||
- depth: 2
|
||||
title: >-
|
||||
Create a new client and attach it to one or more inbounds in a single
|
||||
call. Body is JSON. Per-protocol secrets (UUID for VLESS/VMess, password
|
||||
for Trojan/Shadowsocks, auth for Hysteria) are generated server-side
|
||||
when omitted, so callers can send only the universal fields.
|
||||
url: >-
|
||||
#create-a-new-client-and-attach-it-to-one-or-more-inbounds-in-a-single-call-body-is-json-per-protocol-secrets-uuid-for-vlessvmess-password-for-trojanshadowsocks-auth-for-hysteria-are-generated-server-side-when-omitted-so-callers-can-send-only-the-universal-fields
|
||||
title: Fetch clients by Telegram user ID. Returns an array since multiple
|
||||
clients can share the same Telegram ID.
|
||||
url: '#fetch-clients-by-telegram-user-id-returns-an-array-since-multiple-clients-can-share-the-same-telegram-id'
|
||||
- depth: 2
|
||||
title: >-
|
||||
Update an existing client by email. Changes propagate to every attached
|
||||
title: Create a new client and attach it to one or more inbounds in a single
|
||||
call. Body is JSON. Per-protocol secrets are generated server-side when
|
||||
omitted, so callers can send only the universal fields.
|
||||
url: '#create-a-new-client-and-attach-it-to-one-or-more-inbounds-in-a-single-call-body-is-json-per-protocol-secrets-are-generated-server-side-when-omitted-so-callers-can-send-only-the-universal-fields'
|
||||
- depth: 2
|
||||
title: Update an existing client by email. Changes propagate to every attached
|
||||
inbound. Body is the JSON client payload — supply the full set of fields
|
||||
you want to keep (the server replaces the row, it does not patch).
|
||||
url: >-
|
||||
#update-an-existing-client-by-email-changes-propagate-to-every-attached-inbound-body-is-the-json-client-payload--supply-the-full-set-of-fields-you-want-to-keep-the-server-replaces-the-row-it-does-not-patch
|
||||
url: '#update-an-existing-client-by-email-changes-propagate-to-every-attached-inbound-body-is-the-json-client-payload--supply-the-full-set-of-fields-you-want-to-keep-the-server-replaces-the-row-it-does-not-patch'
|
||||
- depth: 2
|
||||
title: >-
|
||||
Delete a client by email. Removes it from every attached inbound and
|
||||
title: Delete a client by email. Removes it from every attached inbound and
|
||||
drops its traffic record unless keepTraffic=1 is passed.
|
||||
url: >-
|
||||
#delete-a-client-by-email-removes-it-from-every-attached-inbound-and-drops-its-traffic-record-unless-keeptraffic1-is-passed
|
||||
url: '#delete-a-client-by-email-removes-it-from-every-attached-inbound-and-drops-its-traffic-record-unless-keeptraffic1-is-passed'
|
||||
- depth: 2
|
||||
title: >-
|
||||
Attach an existing client to one or more additional inbounds. Body is
|
||||
title: Attach an existing client to one or more additional inbounds. Body is
|
||||
JSON.
|
||||
url: >-
|
||||
#attach-an-existing-client-to-one-or-more-additional-inbounds-body-is-json
|
||||
url: '#attach-an-existing-client-to-one-or-more-additional-inbounds-body-is-json'
|
||||
- depth: 2
|
||||
title: Detach a client from one or more inbounds without deleting the client.
|
||||
url: '#detach-a-client-from-one-or-more-inbounds-without-deleting-the-client'
|
||||
- depth: 2
|
||||
title: >-
|
||||
Replace a client's external links (per-client share links and remote
|
||||
subscription URLs surfaced in their subscription). Sends the full set;
|
||||
the server replaces all rows.
|
||||
url: >-
|
||||
#replace-a-clients-external-links-per-client-share-links-and-remote-subscription-urls-surfaced-in-their-subscription-sends-the-full-set-the-server-replaces-all-rows
|
||||
title: Replace a client's external links and external subscriptions. Sends the
|
||||
full set; the server replaces all rows. Disabled rows stay saved for
|
||||
editing but are not emitted in generated subscriptions.
|
||||
url: '#replace-a-clients-external-links-and-external-subscriptions-sends-the-full-set-the-server-replaces-all-rows-disabled-rows-stay-saved-for-editing-but-are-not-emitted-in-generated-subscriptions'
|
||||
- depth: 2
|
||||
title: >-
|
||||
Reset the up/down counters for every client globally. Quotas and expiry
|
||||
title: Reset the up/down counters for every client globally. Quotas and expiry
|
||||
are not affected. Triggers an Xray restart if any counter actually
|
||||
moved.
|
||||
url: >-
|
||||
#reset-the-updown-counters-for-every-client-globally-quotas-and-expiry-are-not-affected-triggers-an-xray-restart-if-any-counter-actually-moved
|
||||
url: '#reset-the-updown-counters-for-every-client-globally-quotas-and-expiry-are-not-affected-triggers-an-xray-restart-if-any-counter-actually-moved'
|
||||
- depth: 2
|
||||
title: >-
|
||||
Delete every client whose traffic quota is exhausted (used >= total,
|
||||
when reset is disabled) or whose expiry has passed. Returns the deleted
|
||||
count and triggers an Xray restart when any client was on a running
|
||||
inbound.
|
||||
url: >-
|
||||
#delete-every-client-whose-traffic-quota-is-exhausted-used--total-when-reset-is-disabled-or-whose-expiry-has-passed-returns-the-deleted-count-and-triggers-an-xray-restart-when-any-client-was-on-a-running-inbound
|
||||
title: Delete every client whose traffic quota is exhausted (used >= total, when
|
||||
reset is disabled) or whose expiry has passed. Returns the deleted count
|
||||
and triggers an Xray restart when any client was on a running inbound.
|
||||
url: '#delete-every-client-whose-traffic-quota-is-exhausted-used--total-when-reset-is-disabled-or-whose-expiry-has-passed-returns-the-deleted-count-and-triggers-an-xray-restart-when-any-client-was-on-a-running-inbound'
|
||||
- depth: 2
|
||||
title: >-
|
||||
Delete every client that is not attached to any inbound, along with its
|
||||
traffic record, IP log, and external links. Useful for clearing clients
|
||||
left unattached after their inbounds were removed. Returns the deleted
|
||||
count. Cannot be undone.
|
||||
url: >-
|
||||
#delete-every-client-that-is-not-attached-to-any-inbound-along-with-its-traffic-record-ip-log-and-external-links-useful-for-clearing-clients-left-unattached-after-their-inbounds-were-removed-returns-the-deleted-count-cannot-be-undone
|
||||
title: Delete every client that is not attached to any inbound, along with its
|
||||
traffic record, IP log, HWID devices, and external links. Useful for
|
||||
clearing clients left unattached after their inbounds were removed.
|
||||
Returns the deleted count. Cannot be undone.
|
||||
url: '#delete-every-client-that-is-not-attached-to-any-inbound-along-with-its-traffic-record-ip-log-hwid-devices-and-external-links-useful-for-clearing-clients-left-unattached-after-their-inbounds-were-removed-returns-the-deleted-count-cannot-be-undone'
|
||||
- depth: 2
|
||||
title: >-
|
||||
Return every client as a {client, inboundIds} array — the same shape
|
||||
title: Return every client as a {client, inboundIds} array — the same shape
|
||||
/bulkCreate and /import accept — so the payload round-trips straight
|
||||
back through /import. Clients with no inbound attachment are included
|
||||
with an empty inboundIds list. The UI shows this in a CodeMirror viewer
|
||||
(copy / download); programmatic callers get the array in obj.
|
||||
url: >-
|
||||
#return-every-client-as-a-client-inboundids-array--the-same-shape-bulkcreate-and-import-accept--so-the-payload-round-trips-straight-back-through-import-clients-with-no-inbound-attachment-are-included-with-an-empty-inboundids-list-the-ui-shows-this-in-a-codemirror-viewer-copy--download-programmatic-callers-get-the-array-in-obj
|
||||
url: '#return-every-client-as-a-client-inboundids-array--the-same-shape-bulkcreate-and-import-accept--so-the-payload-round-trips-straight-back-through-import-clients-with-no-inbound-attachment-are-included-with-an-empty-inboundids-list-the-ui-shows-this-in-a-codemirror-viewer-copy--download-programmatic-callers-get-the-array-in-obj'
|
||||
- depth: 2
|
||||
title: >-
|
||||
Import clients from a JSON body { "data": "<json>" }, where data is a
|
||||
title: 'Import clients from a JSON body { "data": "<json>" }, where data is a
|
||||
string-encoded array produced by /export ([{client, inboundIds}]). Items
|
||||
with inboundIds are created and attached to those inbounds; items with
|
||||
an empty inboundIds list are restored as unattached client records.
|
||||
Existing emails are never overwritten — they are returned in skipped.
|
||||
Triggers a single Xray restart at the end if any target inbound was
|
||||
running.
|
||||
url: >-
|
||||
#import-clients-from-a-json-body--data-json--where-data-is-a-string-encoded-array-produced-by-export-client-inboundids-items-with-inboundids-are-created-and-attached-to-those-inbounds-items-with-an-empty-inboundids-list-are-restored-as-unattached-client-records-existing-emails-are-never-overwritten--they-are-returned-in-skipped-triggers-a-single-xray-restart-at-the-end-if-any-target-inbound-was-running
|
||||
running.'
|
||||
url: '#import-clients-from-a-json-body--data-json--where-data-is-a-string-encoded-array-produced-by-export-client-inboundids-items-with-inboundids-are-created-and-attached-to-those-inbounds-items-with-an-empty-inboundids-list-are-restored-as-unattached-client-records-existing-emails-are-never-overwritten--they-are-returned-in-skipped-triggers-a-single-xray-restart-at-the-end-if-any-target-inbound-was-running'
|
||||
- depth: 2
|
||||
title: >-
|
||||
Shift expiry and/or traffic quota for many clients in one call.
|
||||
title: 'Shift expiry and/or traffic quota for many clients in one call.
|
||||
addDays/addBytes may be negative. Clients with unlimited expiry
|
||||
(expiryTime=0) or unlimited traffic (totalGB=0) are skipped for the
|
||||
corresponding field — bulk extend never converts unlimited to limited.
|
||||
The optional flow directive sets the XTLS flow on every client: "none"
|
||||
clears it, "xtls-rprx-vision"/"xtls-rprx-vision-udp443" set it where the
|
||||
inbound supports it (omit or "" to leave it unchanged). Returns the
|
||||
adjusted count and per-email skip reasons.
|
||||
url: >-
|
||||
#shift-expiry-andor-traffic-quota-for-many-clients-in-one-call-adddaysaddbytes-may-be-negative-clients-with-unlimited-expiry-expirytime0-or-unlimited-traffic-totalgb0-are-skipped-for-the-corresponding-field--bulk-extend-never-converts-unlimited-to-limited-the-optional-flow-directive-sets-the-xtls-flow-on-every-client-none-clears-it-xtls-rprx-visionxtls-rprx-vision-udp443-set-it-where-the-inbound-supports-it-omit-or--to-leave-it-unchanged-returns-the-adjusted-count-and-per-email-skip-reasons
|
||||
corresponding field — bulk extend never converts unlimited to limited. A
|
||||
client that was auto-disabled solely because it was depleted (expired or
|
||||
over quota) is automatically re-enabled — locally and on its node — when
|
||||
the adjustment lifts it out of depletion; a manually-disabled or
|
||||
still-depleted client is left disabled. The optional flow directive sets
|
||||
the XTLS flow on every client: "none" clears it,
|
||||
"xtls-rprx-vision"/"xtls-rprx-vision-udp443" set it where the inbound
|
||||
supports it (omit or "" to leave it unchanged). Returns the adjusted
|
||||
count and per-email skip reasons.'
|
||||
url: '#shift-expiry-andor-traffic-quota-for-many-clients-in-one-call-adddaysaddbytes-may-be-negative-clients-with-unlimited-expiry-expirytime0-or-unlimited-traffic-totalgb0-are-skipped-for-the-corresponding-field--bulk-extend-never-converts-unlimited-to-limited-a-client-that-was-auto-disabled-solely-because-it-was-depleted-expired-or-over-quota-is-automatically-re-enabled--locally-and-on-its-node--when-the-adjustment-lifts-it-out-of-depletion-a-manually-disabled-or-still-depleted-client-is-left-disabled-the-optional-flow-directive-sets-the-xtls-flow-on-every-client-none-clears-it-xtls-rprx-visionxtls-rprx-vision-udp443-set-it-where-the-inbound-supports-it-omit-or--to-leave-it-unchanged-returns-the-adjusted-count-and-per-email-skip-reasons'
|
||||
- depth: 2
|
||||
title: >-
|
||||
Enable many clients in one call. Emails are grouped by inbound and
|
||||
title: Enable many clients in one call. Emails are grouped by inbound and
|
||||
applied with a single read-modify-write per inbound; the running Xray
|
||||
(local or remote node) is updated to add each user. Note that enabling a
|
||||
client whose quota is exhausted or whose expiry has passed only flips
|
||||
the flag — the traffic loop will disable it again on the next tick.
|
||||
Returns the changed count and per-email skip reasons.
|
||||
url: >-
|
||||
#enable-many-clients-in-one-call-emails-are-grouped-by-inbound-and-applied-with-a-single-read-modify-write-per-inbound-the-running-xray-local-or-remote-node-is-updated-to-add-each-user-note-that-enabling-a-client-whose-quota-is-exhausted-or-whose-expiry-has-passed-only-flips-the-flag--the-traffic-loop-will-disable-it-again-on-the-next-tick-returns-the-changed-count-and-per-email-skip-reasons
|
||||
url: '#enable-many-clients-in-one-call-emails-are-grouped-by-inbound-and-applied-with-a-single-read-modify-write-per-inbound-the-running-xray-local-or-remote-node-is-updated-to-add-each-user-note-that-enabling-a-client-whose-quota-is-exhausted-or-whose-expiry-has-passed-only-flips-the-flag--the-traffic-loop-will-disable-it-again-on-the-next-tick-returns-the-changed-count-and-per-email-skip-reasons'
|
||||
- depth: 2
|
||||
title: >-
|
||||
Disable many clients in one call. Emails are grouped by inbound and
|
||||
title: Disable many clients in one call. Emails are grouped by inbound and
|
||||
applied with a single read-modify-write per inbound; the running Xray
|
||||
(local or remote node) is updated to remove each user. Returns the
|
||||
changed count and per-email skip reasons.
|
||||
url: >-
|
||||
#disable-many-clients-in-one-call-emails-are-grouped-by-inbound-and-applied-with-a-single-read-modify-write-per-inbound-the-running-xray-local-or-remote-node-is-updated-to-remove-each-user-returns-the-changed-count-and-per-email-skip-reasons
|
||||
url: '#disable-many-clients-in-one-call-emails-are-grouped-by-inbound-and-applied-with-a-single-read-modify-write-per-inbound-the-running-xray-local-or-remote-node-is-updated-to-remove-each-user-returns-the-changed-count-and-per-email-skip-reasons'
|
||||
- depth: 2
|
||||
title: >-
|
||||
Delete many clients in one call. The server processes the list
|
||||
title: Delete many clients in one call. The server processes the list
|
||||
sequentially so each delete sees the committed state of the previous one
|
||||
— avoids the race the per-email fan-out had on the panel side. Pass
|
||||
keepTraffic=true to retain the xray_client_traffic rows after deletion.
|
||||
url: >-
|
||||
#delete-many-clients-in-one-call-the-server-processes-the-list-sequentially-so-each-delete-sees-the-committed-state-of-the-previous-one--avoids-the-race-the-per-email-fan-out-had-on-the-panel-side-pass-keeptraffictrue-to-retain-the-xray_client_traffic-rows-after-deletion
|
||||
url: '#delete-many-clients-in-one-call-the-server-processes-the-list-sequentially-so-each-delete-sees-the-committed-state-of-the-previous-one--avoids-the-race-the-per-email-fan-out-had-on-the-panel-side-pass-keeptraffictrue-to-retain-the-xray_client_traffic-rows-after-deletion'
|
||||
- depth: 2
|
||||
title: >-
|
||||
Create many clients in one call. Body is a JSON array of {client,
|
||||
title: Create many clients in one call. Body is a JSON array of {client,
|
||||
inboundIds} payloads — the same shape /add accepts. Items are processed
|
||||
sequentially; per-email skip reasons are returned for items that fail
|
||||
(e.g., duplicate email). Triggers a single Xray restart at the end if
|
||||
any inbound was running.
|
||||
url: >-
|
||||
#create-many-clients-in-one-call-body-is-a-json-array-of-client-inboundids-payloads--the-same-shape-add-accepts-items-are-processed-sequentially-per-email-skip-reasons-are-returned-for-items-that-fail-eg-duplicate-email-triggers-a-single-xray-restart-at-the-end-if-any-inbound-was-running
|
||||
url: '#create-many-clients-in-one-call-body-is-a-json-array-of-client-inboundids-payloads--the-same-shape-add-accepts-items-are-processed-sequentially-per-email-skip-reasons-are-returned-for-items-that-fail-eg-duplicate-email-triggers-a-single-xray-restart-at-the-end-if-any-inbound-was-running'
|
||||
- depth: 2
|
||||
title: >-
|
||||
Add many clients to a group in one call. Updates clients.group_name and
|
||||
title: Add many clients to a group in one call. Updates clients.group_name and
|
||||
patches the matching client entry inside every owning inbound's settings
|
||||
JSON in a single transaction. If the group name does not yet exist (in
|
||||
client_groups or as a derived label), it is auto-created as a persistent
|
||||
group. To clear the group label, use /groups/bulkRemove instead.
|
||||
url: >-
|
||||
#add-many-clients-to-a-group-in-one-call-updates-clientsgroup_name-and-patches-the-matching-client-entry-inside-every-owning-inbounds-settings-json-in-a-single-transaction-if-the-group-name-does-not-yet-exist-in-client_groups-or-as-a-derived-label-it-is-auto-created-as-a-persistent-group-to-clear-the-group-label-use-groupsbulkremove-instead
|
||||
url: '#add-many-clients-to-a-group-in-one-call-updates-clientsgroup_name-and-patches-the-matching-client-entry-inside-every-owning-inbounds-settings-json-in-a-single-transaction-if-the-group-name-does-not-yet-exist-in-client_groups-or-as-a-derived-label-it-is-auto-created-as-a-persistent-group-to-clear-the-group-label-use-groupsbulkremove-instead'
|
||||
- depth: 2
|
||||
title: >-
|
||||
Clear the group label on many clients in one call. Inverse of
|
||||
title: Clear the group label on many clients in one call. Inverse of
|
||||
/groups/bulkAdd. Clients themselves are kept — only the group label is
|
||||
cleared from clients.group_name and from each owning inbound's settings
|
||||
JSON. Groups become empty if all their members are removed.
|
||||
url: >-
|
||||
#clear-the-group-label-on-many-clients-in-one-call-inverse-of-groupsbulkadd-clients-themselves-are-kept--only-the-group-label-is-cleared-from-clientsgroup_name-and-from-each-owning-inbounds-settings-json-groups-become-empty-if-all-their-members-are-removed
|
||||
url: '#clear-the-group-label-on-many-clients-in-one-call-inverse-of-groupsbulkadd-clients-themselves-are-kept--only-the-group-label-is-cleared-from-clientsgroup_name-and-from-each-owning-inbounds-settings-json-groups-become-empty-if-all-their-members-are-removed'
|
||||
- depth: 2
|
||||
title: >-
|
||||
Attach many existing clients to many inbounds in one call. Each client
|
||||
title: Attach many existing clients to many inbounds in one call. Each client
|
||||
keeps its identity (email/UUID/password/subId) and a shared traffic row;
|
||||
all clients are added to a target inbound in a single AddInboundClient
|
||||
call. Clients already present on a target are reported under skipped.
|
||||
Returns per-email attached/skipped/errors lists and triggers a single
|
||||
Xray restart if any target inbound was running.
|
||||
url: >-
|
||||
#attach-many-existing-clients-to-many-inbounds-in-one-call-each-client-keeps-its-identity-emailuuidpasswordsubid-and-a-shared-traffic-row-all-clients-are-added-to-a-target-inbound-in-a-single-addinboundclient-call-clients-already-present-on-a-target-are-reported-under-skipped-returns-per-email-attachedskippederrors-lists-and-triggers-a-single-xray-restart-if-any-target-inbound-was-running
|
||||
url: '#attach-many-existing-clients-to-many-inbounds-in-one-call-each-client-keeps-its-identity-emailuuidpasswordsubid-and-a-shared-traffic-row-all-clients-are-added-to-a-target-inbound-in-a-single-addinboundclient-call-clients-already-present-on-a-target-are-reported-under-skipped-returns-per-email-attachedskippederrors-lists-and-triggers-a-single-xray-restart-if-any-target-inbound-was-running'
|
||||
- depth: 2
|
||||
title: >-
|
||||
Mirror of bulkAttach: detach many existing clients from many inbounds in
|
||||
title: "Mirror of bulkAttach: detach many existing clients from many inbounds in
|
||||
one call. For each email, intersects the client's current inbounds with
|
||||
the requested set and detaches from those only; (email, inbound) pairs
|
||||
where the client is not currently attached are silently no-ops. Emails
|
||||
@@ -197,110 +161,100 @@ _openapi:
|
||||
skipped. Client records are kept even if they become orphaned — use
|
||||
bulkDel for full removal. Returns per-email detached/skipped/errors
|
||||
lists and triggers a single Xray restart if any target inbound was
|
||||
running.
|
||||
url: >-
|
||||
#mirror-of-bulkattach-detach-many-existing-clients-from-many-inbounds-in-one-call-for-each-email-intersects-the-clients-current-inbounds-with-the-requested-set-and-detaches-from-those-only-email-inbound-pairs-where-the-client-is-not-currently-attached-are-silently-no-ops-emails-not-attached-to-any-of-the-requested-inbounds-are-reported-under-skipped-client-records-are-kept-even-if-they-become-orphaned--use-bulkdel-for-full-removal-returns-per-email-detachedskippederrors-lists-and-triggers-a-single-xray-restart-if-any-target-inbound-was-running
|
||||
running."
|
||||
url: '#mirror-of-bulkattach-detach-many-existing-clients-from-many-inbounds-in-one-call-for-each-email-intersects-the-clients-current-inbounds-with-the-requested-set-and-detaches-from-those-only-email-inbound-pairs-where-the-client-is-not-currently-attached-are-silently-no-ops-emails-not-attached-to-any-of-the-requested-inbounds-are-reported-under-skipped-client-records-are-kept-even-if-they-become-orphaned--use-bulkdel-for-full-removal-returns-per-email-detachedskippederrors-lists-and-triggers-a-single-xray-restart-if-any-target-inbound-was-running'
|
||||
- depth: 2
|
||||
title: >-
|
||||
Zero up/down counters for many clients in one call. Loops the
|
||||
title: Zero up/down counters for many clients in one call. Loops the
|
||||
single-reset path so each client is re-enabled across its attached
|
||||
inbounds and pushed to Xray/remote nodes. Returns the count of
|
||||
successfully reset clients.
|
||||
url: >-
|
||||
#zero-updown-counters-for-many-clients-in-one-call-loops-the-single-reset-path-so-each-client-is-re-enabled-across-its-attached-inbounds-and-pushed-to-xrayremote-nodes-returns-the-count-of-successfully-reset-clients
|
||||
url: '#zero-updown-counters-for-many-clients-in-one-call-loops-the-single-reset-path-so-each-client-is-re-enabled-across-its-attached-inbounds-and-pushed-to-xrayremote-nodes-returns-the-count-of-successfully-reset-clients'
|
||||
- depth: 2
|
||||
title: >-
|
||||
List all client groups with their member counts. Merges persisted groups
|
||||
title: List all client groups with their member counts. Merges persisted groups
|
||||
(rows in client_groups, including empty placeholders) with the distinct
|
||||
group_name values currently set on clients. Sorted alphabetically
|
||||
(case-insensitive).
|
||||
url: >-
|
||||
#list-all-client-groups-with-their-member-counts-merges-persisted-groups-rows-in-client_groups-including-empty-placeholders-with-the-distinct-group_name-values-currently-set-on-clients-sorted-alphabetically-case-insensitive
|
||||
url: '#list-all-client-groups-with-their-member-counts-merges-persisted-groups-rows-in-client_groups-including-empty-placeholders-with-the-distinct-group_name-values-currently-set-on-clients-sorted-alphabetically-case-insensitive'
|
||||
- depth: 2
|
||||
title: >-
|
||||
Return just the email list of clients that currently belong to the given
|
||||
title: Return just the email list of clients that currently belong to the given
|
||||
group. Useful for fanning a single bulk action over an entire group
|
||||
without round-tripping the full client list.
|
||||
url: >-
|
||||
#return-just-the-email-list-of-clients-that-currently-belong-to-the-given-group-useful-for-fanning-a-single-bulk-action-over-an-entire-group-without-round-tripping-the-full-client-list
|
||||
url: '#return-just-the-email-list-of-clients-that-currently-belong-to-the-given-group-useful-for-fanning-a-single-bulk-action-over-an-entire-group-without-round-tripping-the-full-client-list'
|
||||
- depth: 2
|
||||
title: >-
|
||||
Create a new empty (placeholder) group. The group becomes selectable in
|
||||
title: Create a new empty (placeholder) group. The group becomes selectable in
|
||||
client forms and the filter drawer even before any client is added to
|
||||
it. Errors if a group with the same name already exists.
|
||||
url: >-
|
||||
#create-a-new-empty-placeholder-group-the-group-becomes-selectable-in-client-forms-and-the-filter-drawer-even-before-any-client-is-added-to-it-errors-if-a-group-with-the-same-name-already-exists
|
||||
url: '#create-a-new-empty-placeholder-group-the-group-becomes-selectable-in-client-forms-and-the-filter-drawer-even-before-any-client-is-added-to-it-errors-if-a-group-with-the-same-name-already-exists'
|
||||
- depth: 2
|
||||
title: >-
|
||||
Rename a group. The new name is applied to the client_groups row AND
|
||||
title: Rename a group. The new name is applied to the client_groups row AND
|
||||
propagated to every matching client (both clients.group_name and the
|
||||
client entry inside every owning inbound's settings JSON) in a single
|
||||
transaction. Returns the number of clients whose label was updated.
|
||||
url: >-
|
||||
#rename-a-group-the-new-name-is-applied-to-the-client_groups-row-and-propagated-to-every-matching-client-both-clientsgroup_name-and-the-client-entry-inside-every-owning-inbounds-settings-json-in-a-single-transaction-returns-the-number-of-clients-whose-label-was-updated
|
||||
url: '#rename-a-group-the-new-name-is-applied-to-the-client_groups-row-and-propagated-to-every-matching-client-both-clientsgroup_name-and-the-client-entry-inside-every-owning-inbounds-settings-json-in-a-single-transaction-returns-the-number-of-clients-whose-label-was-updated'
|
||||
- depth: 2
|
||||
title: >-
|
||||
Remove a group. Deletes the client_groups row and clears the group label
|
||||
title: Remove a group. Deletes the client_groups row and clears the group label
|
||||
from every matching client (both clients.group_name and the inbound
|
||||
settings JSON). The clients themselves are NOT deleted — use /bulkDel
|
||||
after filtering by group for that. Returns the count of clients whose
|
||||
label was cleared.
|
||||
url: >-
|
||||
#remove-a-group-deletes-the-client_groups-row-and-clears-the-group-label-from-every-matching-client-both-clientsgroup_name-and-the-inbound-settings-json-the-clients-themselves-are-not-deleted--use-bulkdel-after-filtering-by-group-for-that-returns-the-count-of-clients-whose-label-was-cleared
|
||||
url: '#remove-a-group-deletes-the-client_groups-row-and-clears-the-group-label-from-every-matching-client-both-clientsgroup_name-and-the-inbound-settings-json-the-clients-themselves-are-not-deleted--use-bulkdel-after-filtering-by-group-for-that-returns-the-count-of-clients-whose-label-was-cleared'
|
||||
- depth: 2
|
||||
title: >-
|
||||
Zero out a single client’s up/down counters. Re-enables the client
|
||||
across every attached inbound and pushes the change to Xray (or the
|
||||
remote node) so depleted users can connect again immediately.
|
||||
url: >-
|
||||
#zero-out-a-single-clients-updown-counters-re-enables-the-client-across-every-attached-inbound-and-pushes-the-change-to-xray-or-the-remote-node-so-depleted-users-can-connect-again-immediately
|
||||
title: Reset only the group-level traffic counter shown on the groups page.
|
||||
Snapshots the current up/down sum of the group's members as a baseline
|
||||
so the group total reads zero, while leaving each client's own counters
|
||||
(and their quotas) untouched. No Xray restart is triggered. Creates the
|
||||
client_groups row if the group exists only as a derived label.
|
||||
url: '#reset-only-the-group-level-traffic-counter-shown-on-the-groups-page-snapshots-the-current-updown-sum-of-the-groups-members-as-a-baseline-so-the-group-total-reads-zero-while-leaving-each-clients-own-counters-and-their-quotas-untouched-no-xray-restart-is-triggered-creates-the-client_groups-row-if-the-group-exists-only-as-a-derived-label'
|
||||
- depth: 2
|
||||
title: >-
|
||||
Manually adjust a client’s upload + download counters. Useful for
|
||||
title: Zero out a single client’s up/down counters. Re-enables the client across
|
||||
every attached inbound and pushes the change to Xray (or the remote
|
||||
node) so depleted users can connect again immediately.
|
||||
url: '#zero-out-a-single-clients-updown-counters-re-enables-the-client-across-every-attached-inbound-and-pushes-the-change-to-xray-or-the-remote-node-so-depleted-users-can-connect-again-immediately'
|
||||
- depth: 2
|
||||
title: Manually adjust a client’s upload + download counters. Useful for
|
||||
migrations from external accounting systems.
|
||||
url: >-
|
||||
#manually-adjust-a-clients-upload--download-counters-useful-for-migrations-from-external-accounting-systems
|
||||
url: '#manually-adjust-a-clients-upload--download-counters-useful-for-migrations-from-external-accounting-systems'
|
||||
- depth: 2
|
||||
title: >-
|
||||
List source IPs that have connected with the given client’s credentials.
|
||||
title: List source IPs that have connected with the given client’s credentials.
|
||||
Returns an array of "ip (timestamp)" strings.
|
||||
url: >-
|
||||
#list-source-ips-that-have-connected-with-the-given-clients-credentials-returns-an-array-of-ip-timestamp-strings
|
||||
url: '#list-source-ips-that-have-connected-with-the-given-clients-credentials-returns-an-array-of-ip-timestamp-strings'
|
||||
- depth: 2
|
||||
title: Reset the recorded IP list for a client.
|
||||
url: '#reset-the-recorded-ip-list-for-a-client'
|
||||
- depth: 2
|
||||
title: >-
|
||||
List the emails of currently connected clients (last seen within the
|
||||
title: List registered HWID devices for a client. Hashes are not exposed.
|
||||
url: '#list-registered-hwid-devices-for-a-client-hashes-are-not-exposed'
|
||||
- depth: 2
|
||||
title: Clear all registered HWID devices for a client so new devices can
|
||||
register again.
|
||||
url: '#clear-all-registered-hwid-devices-for-a-client-so-new-devices-can-register-again'
|
||||
- depth: 2
|
||||
title: Remove a single registered HWID device by its id, freeing one slot under
|
||||
the HWID limit.
|
||||
url: '#remove-a-single-registered-hwid-device-by-its-id-freeing-one-slot-under-the-hwid-limit'
|
||||
- depth: 2
|
||||
title: List the emails of currently connected clients (last seen within the
|
||||
heartbeat window), deduped across every node.
|
||||
url: >-
|
||||
#list-the-emails-of-currently-connected-clients-last-seen-within-the-heartbeat-window-deduped-across-every-node
|
||||
url: '#list-the-emails-of-currently-connected-clients-last-seen-within-the-heartbeat-window-deduped-across-every-node'
|
||||
- depth: 2
|
||||
title: >-
|
||||
Online client emails grouped by the panelGuid of the node that
|
||||
physically hosts each client. The local panel uses its own GUID; each
|
||||
node (at any depth in a chain) uses its GUID. Lets the inbounds page
|
||||
attribute online status to the real node instead of the intermediate one
|
||||
it syncs through.
|
||||
url: >-
|
||||
#online-client-emails-grouped-by-the-panelguid-of-the-node-that-physically-hosts-each-client-the-local-panel-uses-its-own-guid-each-node-at-any-depth-in-a-chain-uses-its-guid-lets-the-inbounds-page-attribute-online-status-to-the-real-node-instead-of-the-intermediate-one-it-syncs-through
|
||||
title: Online client emails grouped by the panelGuid of the node that physically
|
||||
hosts each client. The local panel uses its own GUID; each node (at any
|
||||
depth in a chain) uses its GUID. Lets the inbounds page attribute online
|
||||
status to the real node instead of the intermediate one it syncs
|
||||
through.
|
||||
url: '#online-client-emails-grouped-by-the-panelguid-of-the-node-that-physically-hosts-each-client-the-local-panel-uses-its-own-guid-each-node-at-any-depth-in-a-chain-uses-its-guid-lets-the-inbounds-page-attribute-online-status-to-the-real-node-instead-of-the-intermediate-one-it-syncs-through'
|
||||
- depth: 2
|
||||
title: >-
|
||||
Per-client source IPs grouped by the panelGuid of the node that observed
|
||||
title: Per-client source IPs grouped by the panelGuid of the node that observed
|
||||
them. Lets the central panel attribute and enforce per-client IP limits
|
||||
using the real visitor IPs each node sees, instead of the address of the
|
||||
intermediate panel it syncs through.
|
||||
url: >-
|
||||
#per-client-source-ips-grouped-by-the-panelguid-of-the-node-that-observed-them-lets-the-central-panel-attribute-and-enforce-per-client-ip-limits-using-the-real-visitor-ips-each-node-sees-instead-of-the-address-of-the-intermediate-panel-it-syncs-through
|
||||
url: '#per-client-source-ips-grouped-by-the-panelguid-of-the-node-that-observed-them-lets-the-central-panel-attribute-and-enforce-per-client-ip-limits-using-the-real-visitor-ips-each-node-sees-instead-of-the-address-of-the-intermediate-panel-it-syncs-through'
|
||||
- depth: 2
|
||||
title: >-
|
||||
Inbound tags that carried traffic within the heartbeat window, grouped
|
||||
by the hosting node's panelGuid. Pairs with onlinesByGuid so the
|
||||
inbounds page only marks a multi-inbound client online on the inbounds
|
||||
it actually used. Nodes that do not report per-inbound activity are
|
||||
absent.
|
||||
url: >-
|
||||
#inbound-tags-that-carried-traffic-within-the-heartbeat-window-grouped-by-the-hosting-nodes-panelguid-pairs-with-onlinesbyguid-so-the-inbounds-page-only-marks-a-multi-inbound-client-online-on-the-inbounds-it-actually-used-nodes-that-do-not-report-per-inbound-activity-are-absent
|
||||
title: Inbound tags that carried traffic within the heartbeat window, grouped by
|
||||
the hosting node's panelGuid. Pairs with onlinesByGuid so the inbounds
|
||||
page only marks a multi-inbound client online on the inbounds it
|
||||
actually used. Nodes that do not report per-inbound activity are absent.
|
||||
url: '#inbound-tags-that-carried-traffic-within-the-heartbeat-window-grouped-by-the-hosting-nodes-panelguid-pairs-with-onlinesbyguid-so-the-inbounds-page-only-marks-a-multi-inbound-client-online-on-the-inbounds-it-actually-used-nodes-that-do-not-report-per-inbound-activity-are-absent'
|
||||
- depth: 2
|
||||
title: Map of client email → last-seen unix timestamp.
|
||||
url: '#map-of-client-email--last-seen-unix-timestamp'
|
||||
@@ -308,189 +262,149 @@ _openapi:
|
||||
title: Traffic counters for a client identified by email.
|
||||
url: '#traffic-counters-for-a-client-identified-by-email'
|
||||
- depth: 2
|
||||
title: >-
|
||||
Return every protocol URL (vless://, vmess://, trojan://, ss://,
|
||||
title: Return every protocol URL (vless://, vmess://, trojan://, ss://,
|
||||
hysteria://, hy2://) for clients matching the subscription ID. Same
|
||||
result set as /sub/<subId>, but as a JSON array — no base64. When an
|
||||
inbound has streamSettings.externalProxy set, one URL is emitted per
|
||||
external proxy. Empty array when the subId has no enabled clients.
|
||||
url: >-
|
||||
#return-every-protocol-url-vless-vmess-trojan-ss-hysteria-hy2-for-clients-matching-the-subscription-id-same-result-set-as-subsubid-but-as-a-json-array--no-base64-when-an-inbound-has-streamsettingsexternalproxy-set-one-url-is-emitted-per-external-proxy-empty-array-when-the-subid-has-no-enabled-clients
|
||||
url: '#return-every-protocol-url-vless-vmess-trojan-ss-hysteria-hy2-for-clients-matching-the-subscription-id-same-result-set-as-subsubid-but-as-a-json-array--no-base64-when-an-inbound-has-streamsettingsexternalproxy-set-one-url-is-emitted-per-external-proxy-empty-array-when-the-subid-has-no-enabled-clients'
|
||||
- depth: 2
|
||||
title: >-
|
||||
Return every URL for one client across all attached inbounds — the same
|
||||
title: 'Return every URL for one client across all attached inbounds — the same
|
||||
strings the Copy URL button copies in the panel UI. Supported protocols:
|
||||
vmess, vless, trojan, shadowsocks, hysteria. If
|
||||
streamSettings.externalProxy is set, returns one URL per external proxy.
|
||||
Protocols without a URL form (socks, http, mixed, wireguard, dokodemo,
|
||||
tunnel) contribute nothing.
|
||||
url: >-
|
||||
#return-every-url-for-one-client-across-all-attached-inbounds--the-same-strings-the-copy-url-button-copies-in-the-panel-ui-supported-protocols-vmess-vless-trojan-shadowsocks-hysteria-if-streamsettingsexternalproxy-is-set-returns-one-url-per-external-proxy-protocols-without-a-url-form-socks-http-mixed-wireguard-dokodemo-tunnel-contribute-nothing
|
||||
tunnel) contribute nothing.'
|
||||
url: '#return-every-url-for-one-client-across-all-attached-inbounds--the-same-strings-the-copy-url-button-copies-in-the-panel-ui-supported-protocols-vmess-vless-trojan-shadowsocks-hysteria-if-streamsettingsexternalproxy-is-set-returns-one-url-per-external-proxy-protocols-without-a-url-form-socks-http-mixed-wireguard-dokodemo-tunnel-contribute-nothing'
|
||||
structuredData:
|
||||
headings:
|
||||
- content: >-
|
||||
List every client with its attached inbound IDs and traffic record.
|
||||
The reverse field, if set, is returned as a nested JSON object (legacy
|
||||
- content: List every client with its attached inbound IDs and traffic record. The
|
||||
reverse field, if set, is returned as a nested JSON object (legacy
|
||||
JSON-encoded-string form is still accepted on write).
|
||||
id: >-
|
||||
list-every-client-with-its-attached-inbound-ids-and-traffic-record-the-reverse-field-if-set-is-returned-as-a-nested-json-object-legacy-json-encoded-string-form-is-still-accepted-on-write
|
||||
- content: >-
|
||||
Filter, sort, and paginate clients on the server. Each item is a slim
|
||||
id: list-every-client-with-its-attached-inbound-ids-and-traffic-record-the-reverse-field-if-set-is-returned-as-a-nested-json-object-legacy-json-encoded-string-form-is-still-accepted-on-write
|
||||
- content: 'Filter, sort, and paginate clients on the server. Each item is a slim
|
||||
row (no uuid/password/auth/flow/security/reverse/tgId) so the clients
|
||||
page can ship 25-ish rows in a few KB instead of the full table. The
|
||||
response also includes a summary computed across the full DB row set
|
||||
so dashboard counters stay stable as the user paginates or filters.
|
||||
Page size capped at 200; fetch /get/:email to obtain the full
|
||||
per-client payload for an edit/info modal.
|
||||
id: >-
|
||||
filter-sort-and-paginate-clients-on-the-server-each-item-is-a-slim-row-no-uuidpasswordauthflowsecurityreversetgid-so-the-clients-page-can-ship-25-ish-rows-in-a-few-kb-instead-of-the-full-table-the-response-also-includes-a-summary-computed-across-the-full-db-row-set-so-dashboard-counters-stay-stable-as-the-user-paginates-or-filters-page-size-capped-at-200-fetch-getemail-to-obtain-the-full-per-client-payload-for-an-editinfo-modal
|
||||
- content: >-
|
||||
Fetch one client by email, including the inbound IDs and external
|
||||
so dashboard counters stay stable as the user paginates or filters:
|
||||
the *Count fields are exact, while the email arrays beside them stop
|
||||
at 200 entries so the payload does not grow with the panel. Page size
|
||||
capped at 200; fetch /get/:email to obtain the full per-client payload
|
||||
for an edit/info modal.'
|
||||
id: filter-sort-and-paginate-clients-on-the-server-each-item-is-a-slim-row-no-uuidpasswordauthflowsecurityreversetgid-so-the-clients-page-can-ship-25-ish-rows-in-a-few-kb-instead-of-the-full-table-the-response-also-includes-a-summary-computed-across-the-full-db-row-set-so-dashboard-counters-stay-stable-as-the-user-paginates-or-filters-the-count-fields-are-exact-while-the-email-arrays-beside-them-stop-at-200-entries-so-the-payload-does-not-grow-with-the-panel-page-size-capped-at-200-fetch-getemail-to-obtain-the-full-per-client-payload-for-an-editinfo-modal
|
||||
- content: Fetch one client by email, including the inbound IDs and external
|
||||
config IDs it is attached to.
|
||||
id: >-
|
||||
fetch-one-client-by-email-including-the-inbound-ids-and-external-config-ids-it-is-attached-to
|
||||
- content: >-
|
||||
Create a new client and attach it to one or more inbounds in a single
|
||||
call. Body is JSON. Per-protocol secrets (UUID for VLESS/VMess,
|
||||
password for Trojan/Shadowsocks, auth for Hysteria) are generated
|
||||
server-side when omitted, so callers can send only the universal
|
||||
fields.
|
||||
id: >-
|
||||
create-a-new-client-and-attach-it-to-one-or-more-inbounds-in-a-single-call-body-is-json-per-protocol-secrets-uuid-for-vlessvmess-password-for-trojanshadowsocks-auth-for-hysteria-are-generated-server-side-when-omitted-so-callers-can-send-only-the-universal-fields
|
||||
- content: >-
|
||||
Update an existing client by email. Changes propagate to every
|
||||
attached inbound. Body is the JSON client payload — supply the full
|
||||
set of fields you want to keep (the server replaces the row, it does
|
||||
not patch).
|
||||
id: >-
|
||||
update-an-existing-client-by-email-changes-propagate-to-every-attached-inbound-body-is-the-json-client-payload--supply-the-full-set-of-fields-you-want-to-keep-the-server-replaces-the-row-it-does-not-patch
|
||||
- content: >-
|
||||
Delete a client by email. Removes it from every attached inbound and
|
||||
id: fetch-one-client-by-email-including-the-inbound-ids-and-external-config-ids-it-is-attached-to
|
||||
- content: Fetch clients by Telegram user ID. Returns an array since multiple
|
||||
clients can share the same Telegram ID.
|
||||
id: fetch-clients-by-telegram-user-id-returns-an-array-since-multiple-clients-can-share-the-same-telegram-id
|
||||
- content: Create a new client and attach it to one or more inbounds in a single
|
||||
call. Body is JSON. Per-protocol secrets are generated server-side
|
||||
when omitted, so callers can send only the universal fields.
|
||||
id: create-a-new-client-and-attach-it-to-one-or-more-inbounds-in-a-single-call-body-is-json-per-protocol-secrets-are-generated-server-side-when-omitted-so-callers-can-send-only-the-universal-fields
|
||||
- content: Update an existing client by email. Changes propagate to every attached
|
||||
inbound. Body is the JSON client payload — supply the full set of
|
||||
fields you want to keep (the server replaces the row, it does not
|
||||
patch).
|
||||
id: update-an-existing-client-by-email-changes-propagate-to-every-attached-inbound-body-is-the-json-client-payload--supply-the-full-set-of-fields-you-want-to-keep-the-server-replaces-the-row-it-does-not-patch
|
||||
- content: Delete a client by email. Removes it from every attached inbound and
|
||||
drops its traffic record unless keepTraffic=1 is passed.
|
||||
id: >-
|
||||
delete-a-client-by-email-removes-it-from-every-attached-inbound-and-drops-its-traffic-record-unless-keeptraffic1-is-passed
|
||||
- content: >-
|
||||
Attach an existing client to one or more additional inbounds. Body is
|
||||
id: delete-a-client-by-email-removes-it-from-every-attached-inbound-and-drops-its-traffic-record-unless-keeptraffic1-is-passed
|
||||
- content: Attach an existing client to one or more additional inbounds. Body is
|
||||
JSON.
|
||||
id: >-
|
||||
attach-an-existing-client-to-one-or-more-additional-inbounds-body-is-json
|
||||
id: attach-an-existing-client-to-one-or-more-additional-inbounds-body-is-json
|
||||
- content: Detach a client from one or more inbounds without deleting the client.
|
||||
id: detach-a-client-from-one-or-more-inbounds-without-deleting-the-client
|
||||
- content: >-
|
||||
Replace a client's external links (per-client share links and remote
|
||||
subscription URLs surfaced in their subscription). Sends the full set;
|
||||
the server replaces all rows.
|
||||
id: >-
|
||||
replace-a-clients-external-links-per-client-share-links-and-remote-subscription-urls-surfaced-in-their-subscription-sends-the-full-set-the-server-replaces-all-rows
|
||||
- content: >-
|
||||
Reset the up/down counters for every client globally. Quotas and
|
||||
expiry are not affected. Triggers an Xray restart if any counter
|
||||
actually moved.
|
||||
id: >-
|
||||
reset-the-updown-counters-for-every-client-globally-quotas-and-expiry-are-not-affected-triggers-an-xray-restart-if-any-counter-actually-moved
|
||||
- content: >-
|
||||
Delete every client whose traffic quota is exhausted (used >= total,
|
||||
- content: Replace a client's external links and external subscriptions. Sends the
|
||||
full set; the server replaces all rows. Disabled rows stay saved for
|
||||
editing but are not emitted in generated subscriptions.
|
||||
id: replace-a-clients-external-links-and-external-subscriptions-sends-the-full-set-the-server-replaces-all-rows-disabled-rows-stay-saved-for-editing-but-are-not-emitted-in-generated-subscriptions
|
||||
- content: Reset the up/down counters for every client globally. Quotas and expiry
|
||||
are not affected. Triggers an Xray restart if any counter actually
|
||||
moved.
|
||||
id: reset-the-updown-counters-for-every-client-globally-quotas-and-expiry-are-not-affected-triggers-an-xray-restart-if-any-counter-actually-moved
|
||||
- content: Delete every client whose traffic quota is exhausted (used >= total,
|
||||
when reset is disabled) or whose expiry has passed. Returns the
|
||||
deleted count and triggers an Xray restart when any client was on a
|
||||
running inbound.
|
||||
id: >-
|
||||
delete-every-client-whose-traffic-quota-is-exhausted-used--total-when-reset-is-disabled-or-whose-expiry-has-passed-returns-the-deleted-count-and-triggers-an-xray-restart-when-any-client-was-on-a-running-inbound
|
||||
- content: >-
|
||||
Delete every client that is not attached to any inbound, along with
|
||||
its traffic record, IP log, and external links. Useful for clearing
|
||||
clients left unattached after their inbounds were removed. Returns the
|
||||
deleted count. Cannot be undone.
|
||||
id: >-
|
||||
delete-every-client-that-is-not-attached-to-any-inbound-along-with-its-traffic-record-ip-log-and-external-links-useful-for-clearing-clients-left-unattached-after-their-inbounds-were-removed-returns-the-deleted-count-cannot-be-undone
|
||||
- content: >-
|
||||
Return every client as a {client, inboundIds} array — the same shape
|
||||
id: delete-every-client-whose-traffic-quota-is-exhausted-used--total-when-reset-is-disabled-or-whose-expiry-has-passed-returns-the-deleted-count-and-triggers-an-xray-restart-when-any-client-was-on-a-running-inbound
|
||||
- content: Delete every client that is not attached to any inbound, along with its
|
||||
traffic record, IP log, HWID devices, and external links. Useful for
|
||||
clearing clients left unattached after their inbounds were removed.
|
||||
Returns the deleted count. Cannot be undone.
|
||||
id: delete-every-client-that-is-not-attached-to-any-inbound-along-with-its-traffic-record-ip-log-hwid-devices-and-external-links-useful-for-clearing-clients-left-unattached-after-their-inbounds-were-removed-returns-the-deleted-count-cannot-be-undone
|
||||
- content: Return every client as a {client, inboundIds} array — the same shape
|
||||
/bulkCreate and /import accept — so the payload round-trips straight
|
||||
back through /import. Clients with no inbound attachment are included
|
||||
with an empty inboundIds list. The UI shows this in a CodeMirror
|
||||
viewer (copy / download); programmatic callers get the array in obj.
|
||||
id: >-
|
||||
return-every-client-as-a-client-inboundids-array--the-same-shape-bulkcreate-and-import-accept--so-the-payload-round-trips-straight-back-through-import-clients-with-no-inbound-attachment-are-included-with-an-empty-inboundids-list-the-ui-shows-this-in-a-codemirror-viewer-copy--download-programmatic-callers-get-the-array-in-obj
|
||||
- content: >-
|
||||
Import clients from a JSON body { "data": "<json>" }, where data is a
|
||||
id: return-every-client-as-a-client-inboundids-array--the-same-shape-bulkcreate-and-import-accept--so-the-payload-round-trips-straight-back-through-import-clients-with-no-inbound-attachment-are-included-with-an-empty-inboundids-list-the-ui-shows-this-in-a-codemirror-viewer-copy--download-programmatic-callers-get-the-array-in-obj
|
||||
- content: 'Import clients from a JSON body { "data": "<json>" }, where data is a
|
||||
string-encoded array produced by /export ([{client, inboundIds}]).
|
||||
Items with inboundIds are created and attached to those inbounds;
|
||||
items with an empty inboundIds list are restored as unattached client
|
||||
records. Existing emails are never overwritten — they are returned in
|
||||
skipped. Triggers a single Xray restart at the end if any target
|
||||
inbound was running.
|
||||
id: >-
|
||||
import-clients-from-a-json-body--data-json--where-data-is-a-string-encoded-array-produced-by-export-client-inboundids-items-with-inboundids-are-created-and-attached-to-those-inbounds-items-with-an-empty-inboundids-list-are-restored-as-unattached-client-records-existing-emails-are-never-overwritten--they-are-returned-in-skipped-triggers-a-single-xray-restart-at-the-end-if-any-target-inbound-was-running
|
||||
- content: >-
|
||||
Shift expiry and/or traffic quota for many clients in one call.
|
||||
inbound was running.'
|
||||
id: import-clients-from-a-json-body--data-json--where-data-is-a-string-encoded-array-produced-by-export-client-inboundids-items-with-inboundids-are-created-and-attached-to-those-inbounds-items-with-an-empty-inboundids-list-are-restored-as-unattached-client-records-existing-emails-are-never-overwritten--they-are-returned-in-skipped-triggers-a-single-xray-restart-at-the-end-if-any-target-inbound-was-running
|
||||
- content: 'Shift expiry and/or traffic quota for many clients in one call.
|
||||
addDays/addBytes may be negative. Clients with unlimited expiry
|
||||
(expiryTime=0) or unlimited traffic (totalGB=0) are skipped for the
|
||||
corresponding field — bulk extend never converts unlimited to limited.
|
||||
The optional flow directive sets the XTLS flow on every client: "none"
|
||||
A client that was auto-disabled solely because it was depleted
|
||||
(expired or over quota) is automatically re-enabled — locally and on
|
||||
its node — when the adjustment lifts it out of depletion; a
|
||||
manually-disabled or still-depleted client is left disabled. The
|
||||
optional flow directive sets the XTLS flow on every client: "none"
|
||||
clears it, "xtls-rprx-vision"/"xtls-rprx-vision-udp443" set it where
|
||||
the inbound supports it (omit or "" to leave it unchanged). Returns
|
||||
the adjusted count and per-email skip reasons.
|
||||
id: >-
|
||||
shift-expiry-andor-traffic-quota-for-many-clients-in-one-call-adddaysaddbytes-may-be-negative-clients-with-unlimited-expiry-expirytime0-or-unlimited-traffic-totalgb0-are-skipped-for-the-corresponding-field--bulk-extend-never-converts-unlimited-to-limited-the-optional-flow-directive-sets-the-xtls-flow-on-every-client-none-clears-it-xtls-rprx-visionxtls-rprx-vision-udp443-set-it-where-the-inbound-supports-it-omit-or--to-leave-it-unchanged-returns-the-adjusted-count-and-per-email-skip-reasons
|
||||
- content: >-
|
||||
Enable many clients in one call. Emails are grouped by inbound and
|
||||
the adjusted count and per-email skip reasons.'
|
||||
id: shift-expiry-andor-traffic-quota-for-many-clients-in-one-call-adddaysaddbytes-may-be-negative-clients-with-unlimited-expiry-expirytime0-or-unlimited-traffic-totalgb0-are-skipped-for-the-corresponding-field--bulk-extend-never-converts-unlimited-to-limited-a-client-that-was-auto-disabled-solely-because-it-was-depleted-expired-or-over-quota-is-automatically-re-enabled--locally-and-on-its-node--when-the-adjustment-lifts-it-out-of-depletion-a-manually-disabled-or-still-depleted-client-is-left-disabled-the-optional-flow-directive-sets-the-xtls-flow-on-every-client-none-clears-it-xtls-rprx-visionxtls-rprx-vision-udp443-set-it-where-the-inbound-supports-it-omit-or--to-leave-it-unchanged-returns-the-adjusted-count-and-per-email-skip-reasons
|
||||
- content: Enable many clients in one call. Emails are grouped by inbound and
|
||||
applied with a single read-modify-write per inbound; the running Xray
|
||||
(local or remote node) is updated to add each user. Note that enabling
|
||||
a client whose quota is exhausted or whose expiry has passed only
|
||||
flips the flag — the traffic loop will disable it again on the next
|
||||
tick. Returns the changed count and per-email skip reasons.
|
||||
id: >-
|
||||
enable-many-clients-in-one-call-emails-are-grouped-by-inbound-and-applied-with-a-single-read-modify-write-per-inbound-the-running-xray-local-or-remote-node-is-updated-to-add-each-user-note-that-enabling-a-client-whose-quota-is-exhausted-or-whose-expiry-has-passed-only-flips-the-flag--the-traffic-loop-will-disable-it-again-on-the-next-tick-returns-the-changed-count-and-per-email-skip-reasons
|
||||
- content: >-
|
||||
Disable many clients in one call. Emails are grouped by inbound and
|
||||
id: enable-many-clients-in-one-call-emails-are-grouped-by-inbound-and-applied-with-a-single-read-modify-write-per-inbound-the-running-xray-local-or-remote-node-is-updated-to-add-each-user-note-that-enabling-a-client-whose-quota-is-exhausted-or-whose-expiry-has-passed-only-flips-the-flag--the-traffic-loop-will-disable-it-again-on-the-next-tick-returns-the-changed-count-and-per-email-skip-reasons
|
||||
- content: Disable many clients in one call. Emails are grouped by inbound and
|
||||
applied with a single read-modify-write per inbound; the running Xray
|
||||
(local or remote node) is updated to remove each user. Returns the
|
||||
changed count and per-email skip reasons.
|
||||
id: >-
|
||||
disable-many-clients-in-one-call-emails-are-grouped-by-inbound-and-applied-with-a-single-read-modify-write-per-inbound-the-running-xray-local-or-remote-node-is-updated-to-remove-each-user-returns-the-changed-count-and-per-email-skip-reasons
|
||||
- content: >-
|
||||
Delete many clients in one call. The server processes the list
|
||||
id: disable-many-clients-in-one-call-emails-are-grouped-by-inbound-and-applied-with-a-single-read-modify-write-per-inbound-the-running-xray-local-or-remote-node-is-updated-to-remove-each-user-returns-the-changed-count-and-per-email-skip-reasons
|
||||
- content: Delete many clients in one call. The server processes the list
|
||||
sequentially so each delete sees the committed state of the previous
|
||||
one — avoids the race the per-email fan-out had on the panel side.
|
||||
Pass keepTraffic=true to retain the xray_client_traffic rows after
|
||||
deletion.
|
||||
id: >-
|
||||
delete-many-clients-in-one-call-the-server-processes-the-list-sequentially-so-each-delete-sees-the-committed-state-of-the-previous-one--avoids-the-race-the-per-email-fan-out-had-on-the-panel-side-pass-keeptraffictrue-to-retain-the-xray_client_traffic-rows-after-deletion
|
||||
- content: >-
|
||||
Create many clients in one call. Body is a JSON array of {client,
|
||||
id: delete-many-clients-in-one-call-the-server-processes-the-list-sequentially-so-each-delete-sees-the-committed-state-of-the-previous-one--avoids-the-race-the-per-email-fan-out-had-on-the-panel-side-pass-keeptraffictrue-to-retain-the-xray_client_traffic-rows-after-deletion
|
||||
- content: Create many clients in one call. Body is a JSON array of {client,
|
||||
inboundIds} payloads — the same shape /add accepts. Items are
|
||||
processed sequentially; per-email skip reasons are returned for items
|
||||
that fail (e.g., duplicate email). Triggers a single Xray restart at
|
||||
the end if any inbound was running.
|
||||
id: >-
|
||||
create-many-clients-in-one-call-body-is-a-json-array-of-client-inboundids-payloads--the-same-shape-add-accepts-items-are-processed-sequentially-per-email-skip-reasons-are-returned-for-items-that-fail-eg-duplicate-email-triggers-a-single-xray-restart-at-the-end-if-any-inbound-was-running
|
||||
- content: >-
|
||||
Add many clients to a group in one call. Updates clients.group_name
|
||||
and patches the matching client entry inside every owning inbound's
|
||||
id: create-many-clients-in-one-call-body-is-a-json-array-of-client-inboundids-payloads--the-same-shape-add-accepts-items-are-processed-sequentially-per-email-skip-reasons-are-returned-for-items-that-fail-eg-duplicate-email-triggers-a-single-xray-restart-at-the-end-if-any-inbound-was-running
|
||||
- content: Add many clients to a group in one call. Updates clients.group_name and
|
||||
patches the matching client entry inside every owning inbound's
|
||||
settings JSON in a single transaction. If the group name does not yet
|
||||
exist (in client_groups or as a derived label), it is auto-created as
|
||||
a persistent group. To clear the group label, use /groups/bulkRemove
|
||||
instead.
|
||||
id: >-
|
||||
add-many-clients-to-a-group-in-one-call-updates-clientsgroup_name-and-patches-the-matching-client-entry-inside-every-owning-inbounds-settings-json-in-a-single-transaction-if-the-group-name-does-not-yet-exist-in-client_groups-or-as-a-derived-label-it-is-auto-created-as-a-persistent-group-to-clear-the-group-label-use-groupsbulkremove-instead
|
||||
- content: >-
|
||||
Clear the group label on many clients in one call. Inverse of
|
||||
id: add-many-clients-to-a-group-in-one-call-updates-clientsgroup_name-and-patches-the-matching-client-entry-inside-every-owning-inbounds-settings-json-in-a-single-transaction-if-the-group-name-does-not-yet-exist-in-client_groups-or-as-a-derived-label-it-is-auto-created-as-a-persistent-group-to-clear-the-group-label-use-groupsbulkremove-instead
|
||||
- content: Clear the group label on many clients in one call. Inverse of
|
||||
/groups/bulkAdd. Clients themselves are kept — only the group label is
|
||||
cleared from clients.group_name and from each owning inbound's
|
||||
settings JSON. Groups become empty if all their members are removed.
|
||||
id: >-
|
||||
clear-the-group-label-on-many-clients-in-one-call-inverse-of-groupsbulkadd-clients-themselves-are-kept--only-the-group-label-is-cleared-from-clientsgroup_name-and-from-each-owning-inbounds-settings-json-groups-become-empty-if-all-their-members-are-removed
|
||||
- content: >-
|
||||
Attach many existing clients to many inbounds in one call. Each client
|
||||
id: clear-the-group-label-on-many-clients-in-one-call-inverse-of-groupsbulkadd-clients-themselves-are-kept--only-the-group-label-is-cleared-from-clientsgroup_name-and-from-each-owning-inbounds-settings-json-groups-become-empty-if-all-their-members-are-removed
|
||||
- content: Attach many existing clients to many inbounds in one call. Each client
|
||||
keeps its identity (email/UUID/password/subId) and a shared traffic
|
||||
row; all clients are added to a target inbound in a single
|
||||
AddInboundClient call. Clients already present on a target are
|
||||
reported under skipped. Returns per-email attached/skipped/errors
|
||||
lists and triggers a single Xray restart if any target inbound was
|
||||
running.
|
||||
id: >-
|
||||
attach-many-existing-clients-to-many-inbounds-in-one-call-each-client-keeps-its-identity-emailuuidpasswordsubid-and-a-shared-traffic-row-all-clients-are-added-to-a-target-inbound-in-a-single-addinboundclient-call-clients-already-present-on-a-target-are-reported-under-skipped-returns-per-email-attachedskippederrors-lists-and-triggers-a-single-xray-restart-if-any-target-inbound-was-running
|
||||
- content: >-
|
||||
Mirror of bulkAttach: detach many existing clients from many inbounds
|
||||
id: attach-many-existing-clients-to-many-inbounds-in-one-call-each-client-keeps-its-identity-emailuuidpasswordsubid-and-a-shared-traffic-row-all-clients-are-added-to-a-target-inbound-in-a-single-addinboundclient-call-clients-already-present-on-a-target-are-reported-under-skipped-returns-per-email-attachedskippederrors-lists-and-triggers-a-single-xray-restart-if-any-target-inbound-was-running
|
||||
- content: "Mirror of bulkAttach: detach many existing clients from many inbounds
|
||||
in one call. For each email, intersects the client's current inbounds
|
||||
with the requested set and detaches from those only; (email, inbound)
|
||||
pairs where the client is not currently attached are silently no-ops.
|
||||
@@ -498,118 +412,152 @@ _openapi:
|
||||
under skipped. Client records are kept even if they become orphaned —
|
||||
use bulkDel for full removal. Returns per-email
|
||||
detached/skipped/errors lists and triggers a single Xray restart if
|
||||
any target inbound was running.
|
||||
id: >-
|
||||
mirror-of-bulkattach-detach-many-existing-clients-from-many-inbounds-in-one-call-for-each-email-intersects-the-clients-current-inbounds-with-the-requested-set-and-detaches-from-those-only-email-inbound-pairs-where-the-client-is-not-currently-attached-are-silently-no-ops-emails-not-attached-to-any-of-the-requested-inbounds-are-reported-under-skipped-client-records-are-kept-even-if-they-become-orphaned--use-bulkdel-for-full-removal-returns-per-email-detachedskippederrors-lists-and-triggers-a-single-xray-restart-if-any-target-inbound-was-running
|
||||
- content: >-
|
||||
Zero up/down counters for many clients in one call. Loops the
|
||||
any target inbound was running."
|
||||
id: mirror-of-bulkattach-detach-many-existing-clients-from-many-inbounds-in-one-call-for-each-email-intersects-the-clients-current-inbounds-with-the-requested-set-and-detaches-from-those-only-email-inbound-pairs-where-the-client-is-not-currently-attached-are-silently-no-ops-emails-not-attached-to-any-of-the-requested-inbounds-are-reported-under-skipped-client-records-are-kept-even-if-they-become-orphaned--use-bulkdel-for-full-removal-returns-per-email-detachedskippederrors-lists-and-triggers-a-single-xray-restart-if-any-target-inbound-was-running
|
||||
- content: Zero up/down counters for many clients in one call. Loops the
|
||||
single-reset path so each client is re-enabled across its attached
|
||||
inbounds and pushed to Xray/remote nodes. Returns the count of
|
||||
successfully reset clients.
|
||||
id: >-
|
||||
zero-updown-counters-for-many-clients-in-one-call-loops-the-single-reset-path-so-each-client-is-re-enabled-across-its-attached-inbounds-and-pushed-to-xrayremote-nodes-returns-the-count-of-successfully-reset-clients
|
||||
- content: >-
|
||||
List all client groups with their member counts. Merges persisted
|
||||
id: zero-updown-counters-for-many-clients-in-one-call-loops-the-single-reset-path-so-each-client-is-re-enabled-across-its-attached-inbounds-and-pushed-to-xrayremote-nodes-returns-the-count-of-successfully-reset-clients
|
||||
- content: List all client groups with their member counts. Merges persisted
|
||||
groups (rows in client_groups, including empty placeholders) with the
|
||||
distinct group_name values currently set on clients. Sorted
|
||||
alphabetically (case-insensitive).
|
||||
id: >-
|
||||
list-all-client-groups-with-their-member-counts-merges-persisted-groups-rows-in-client_groups-including-empty-placeholders-with-the-distinct-group_name-values-currently-set-on-clients-sorted-alphabetically-case-insensitive
|
||||
- content: >-
|
||||
Return just the email list of clients that currently belong to the
|
||||
id: list-all-client-groups-with-their-member-counts-merges-persisted-groups-rows-in-client_groups-including-empty-placeholders-with-the-distinct-group_name-values-currently-set-on-clients-sorted-alphabetically-case-insensitive
|
||||
- content: Return just the email list of clients that currently belong to the
|
||||
given group. Useful for fanning a single bulk action over an entire
|
||||
group without round-tripping the full client list.
|
||||
id: >-
|
||||
return-just-the-email-list-of-clients-that-currently-belong-to-the-given-group-useful-for-fanning-a-single-bulk-action-over-an-entire-group-without-round-tripping-the-full-client-list
|
||||
- content: >-
|
||||
Create a new empty (placeholder) group. The group becomes selectable
|
||||
in client forms and the filter drawer even before any client is added
|
||||
to it. Errors if a group with the same name already exists.
|
||||
id: >-
|
||||
create-a-new-empty-placeholder-group-the-group-becomes-selectable-in-client-forms-and-the-filter-drawer-even-before-any-client-is-added-to-it-errors-if-a-group-with-the-same-name-already-exists
|
||||
- content: >-
|
||||
Rename a group. The new name is applied to the client_groups row AND
|
||||
id: return-just-the-email-list-of-clients-that-currently-belong-to-the-given-group-useful-for-fanning-a-single-bulk-action-over-an-entire-group-without-round-tripping-the-full-client-list
|
||||
- content: Create a new empty (placeholder) group. The group becomes selectable in
|
||||
client forms and the filter drawer even before any client is added to
|
||||
it. Errors if a group with the same name already exists.
|
||||
id: create-a-new-empty-placeholder-group-the-group-becomes-selectable-in-client-forms-and-the-filter-drawer-even-before-any-client-is-added-to-it-errors-if-a-group-with-the-same-name-already-exists
|
||||
- content: Rename a group. The new name is applied to the client_groups row AND
|
||||
propagated to every matching client (both clients.group_name and the
|
||||
client entry inside every owning inbound's settings JSON) in a single
|
||||
transaction. Returns the number of clients whose label was updated.
|
||||
id: >-
|
||||
rename-a-group-the-new-name-is-applied-to-the-client_groups-row-and-propagated-to-every-matching-client-both-clientsgroup_name-and-the-client-entry-inside-every-owning-inbounds-settings-json-in-a-single-transaction-returns-the-number-of-clients-whose-label-was-updated
|
||||
- content: >-
|
||||
Remove a group. Deletes the client_groups row and clears the group
|
||||
id: rename-a-group-the-new-name-is-applied-to-the-client_groups-row-and-propagated-to-every-matching-client-both-clientsgroup_name-and-the-client-entry-inside-every-owning-inbounds-settings-json-in-a-single-transaction-returns-the-number-of-clients-whose-label-was-updated
|
||||
- content: Remove a group. Deletes the client_groups row and clears the group
|
||||
label from every matching client (both clients.group_name and the
|
||||
inbound settings JSON). The clients themselves are NOT deleted — use
|
||||
/bulkDel after filtering by group for that. Returns the count of
|
||||
clients whose label was cleared.
|
||||
id: >-
|
||||
remove-a-group-deletes-the-client_groups-row-and-clears-the-group-label-from-every-matching-client-both-clientsgroup_name-and-the-inbound-settings-json-the-clients-themselves-are-not-deleted--use-bulkdel-after-filtering-by-group-for-that-returns-the-count-of-clients-whose-label-was-cleared
|
||||
- content: >-
|
||||
Zero out a single client’s up/down counters. Re-enables the client
|
||||
id: remove-a-group-deletes-the-client_groups-row-and-clears-the-group-label-from-every-matching-client-both-clientsgroup_name-and-the-inbound-settings-json-the-clients-themselves-are-not-deleted--use-bulkdel-after-filtering-by-group-for-that-returns-the-count-of-clients-whose-label-was-cleared
|
||||
- content: Reset only the group-level traffic counter shown on the groups page.
|
||||
Snapshots the current up/down sum of the group's members as a baseline
|
||||
so the group total reads zero, while leaving each client's own
|
||||
counters (and their quotas) untouched. No Xray restart is triggered.
|
||||
Creates the client_groups row if the group exists only as a derived
|
||||
label.
|
||||
id: reset-only-the-group-level-traffic-counter-shown-on-the-groups-page-snapshots-the-current-updown-sum-of-the-groups-members-as-a-baseline-so-the-group-total-reads-zero-while-leaving-each-clients-own-counters-and-their-quotas-untouched-no-xray-restart-is-triggered-creates-the-client_groups-row-if-the-group-exists-only-as-a-derived-label
|
||||
- content: Zero out a single client’s up/down counters. Re-enables the client
|
||||
across every attached inbound and pushes the change to Xray (or the
|
||||
remote node) so depleted users can connect again immediately.
|
||||
id: >-
|
||||
zero-out-a-single-clients-updown-counters-re-enables-the-client-across-every-attached-inbound-and-pushes-the-change-to-xray-or-the-remote-node-so-depleted-users-can-connect-again-immediately
|
||||
- content: >-
|
||||
Manually adjust a client’s upload + download counters. Useful for
|
||||
id: zero-out-a-single-clients-updown-counters-re-enables-the-client-across-every-attached-inbound-and-pushes-the-change-to-xray-or-the-remote-node-so-depleted-users-can-connect-again-immediately
|
||||
- content: Manually adjust a client’s upload + download counters. Useful for
|
||||
migrations from external accounting systems.
|
||||
id: >-
|
||||
manually-adjust-a-clients-upload--download-counters-useful-for-migrations-from-external-accounting-systems
|
||||
- content: >-
|
||||
List source IPs that have connected with the given client’s
|
||||
id: manually-adjust-a-clients-upload--download-counters-useful-for-migrations-from-external-accounting-systems
|
||||
- content: List source IPs that have connected with the given client’s
|
||||
credentials. Returns an array of "ip (timestamp)" strings.
|
||||
id: >-
|
||||
list-source-ips-that-have-connected-with-the-given-clients-credentials-returns-an-array-of-ip-timestamp-strings
|
||||
id: list-source-ips-that-have-connected-with-the-given-clients-credentials-returns-an-array-of-ip-timestamp-strings
|
||||
- content: Reset the recorded IP list for a client.
|
||||
id: reset-the-recorded-ip-list-for-a-client
|
||||
- content: >-
|
||||
List the emails of currently connected clients (last seen within the
|
||||
- content: List registered HWID devices for a client. Hashes are not exposed.
|
||||
id: list-registered-hwid-devices-for-a-client-hashes-are-not-exposed
|
||||
- content: Clear all registered HWID devices for a client so new devices can
|
||||
register again.
|
||||
id: clear-all-registered-hwid-devices-for-a-client-so-new-devices-can-register-again
|
||||
- content: Remove a single registered HWID device by its id, freeing one slot
|
||||
under the HWID limit.
|
||||
id: remove-a-single-registered-hwid-device-by-its-id-freeing-one-slot-under-the-hwid-limit
|
||||
- content: List the emails of currently connected clients (last seen within the
|
||||
heartbeat window), deduped across every node.
|
||||
id: >-
|
||||
list-the-emails-of-currently-connected-clients-last-seen-within-the-heartbeat-window-deduped-across-every-node
|
||||
- content: >-
|
||||
Online client emails grouped by the panelGuid of the node that
|
||||
id: list-the-emails-of-currently-connected-clients-last-seen-within-the-heartbeat-window-deduped-across-every-node
|
||||
- content: Online client emails grouped by the panelGuid of the node that
|
||||
physically hosts each client. The local panel uses its own GUID; each
|
||||
node (at any depth in a chain) uses its GUID. Lets the inbounds page
|
||||
attribute online status to the real node instead of the intermediate
|
||||
one it syncs through.
|
||||
id: >-
|
||||
online-client-emails-grouped-by-the-panelguid-of-the-node-that-physically-hosts-each-client-the-local-panel-uses-its-own-guid-each-node-at-any-depth-in-a-chain-uses-its-guid-lets-the-inbounds-page-attribute-online-status-to-the-real-node-instead-of-the-intermediate-one-it-syncs-through
|
||||
- content: >-
|
||||
Per-client source IPs grouped by the panelGuid of the node that
|
||||
id: online-client-emails-grouped-by-the-panelguid-of-the-node-that-physically-hosts-each-client-the-local-panel-uses-its-own-guid-each-node-at-any-depth-in-a-chain-uses-its-guid-lets-the-inbounds-page-attribute-online-status-to-the-real-node-instead-of-the-intermediate-one-it-syncs-through
|
||||
- content: Per-client source IPs grouped by the panelGuid of the node that
|
||||
observed them. Lets the central panel attribute and enforce per-client
|
||||
IP limits using the real visitor IPs each node sees, instead of the
|
||||
address of the intermediate panel it syncs through.
|
||||
id: >-
|
||||
per-client-source-ips-grouped-by-the-panelguid-of-the-node-that-observed-them-lets-the-central-panel-attribute-and-enforce-per-client-ip-limits-using-the-real-visitor-ips-each-node-sees-instead-of-the-address-of-the-intermediate-panel-it-syncs-through
|
||||
- content: >-
|
||||
Inbound tags that carried traffic within the heartbeat window, grouped
|
||||
id: per-client-source-ips-grouped-by-the-panelguid-of-the-node-that-observed-them-lets-the-central-panel-attribute-and-enforce-per-client-ip-limits-using-the-real-visitor-ips-each-node-sees-instead-of-the-address-of-the-intermediate-panel-it-syncs-through
|
||||
- content: Inbound tags that carried traffic within the heartbeat window, grouped
|
||||
by the hosting node's panelGuid. Pairs with onlinesByGuid so the
|
||||
inbounds page only marks a multi-inbound client online on the inbounds
|
||||
it actually used. Nodes that do not report per-inbound activity are
|
||||
absent.
|
||||
id: >-
|
||||
inbound-tags-that-carried-traffic-within-the-heartbeat-window-grouped-by-the-hosting-nodes-panelguid-pairs-with-onlinesbyguid-so-the-inbounds-page-only-marks-a-multi-inbound-client-online-on-the-inbounds-it-actually-used-nodes-that-do-not-report-per-inbound-activity-are-absent
|
||||
id: inbound-tags-that-carried-traffic-within-the-heartbeat-window-grouped-by-the-hosting-nodes-panelguid-pairs-with-onlinesbyguid-so-the-inbounds-page-only-marks-a-multi-inbound-client-online-on-the-inbounds-it-actually-used-nodes-that-do-not-report-per-inbound-activity-are-absent
|
||||
- content: Map of client email → last-seen unix timestamp.
|
||||
id: map-of-client-email--last-seen-unix-timestamp
|
||||
- content: Traffic counters for a client identified by email.
|
||||
id: traffic-counters-for-a-client-identified-by-email
|
||||
- content: >-
|
||||
Return every protocol URL (vless://, vmess://, trojan://, ss://,
|
||||
- content: Return every protocol URL (vless://, vmess://, trojan://, ss://,
|
||||
hysteria://, hy2://) for clients matching the subscription ID. Same
|
||||
result set as /sub/<subId>, but as a JSON array — no base64. When an
|
||||
inbound has streamSettings.externalProxy set, one URL is emitted per
|
||||
external proxy. Empty array when the subId has no enabled clients.
|
||||
id: >-
|
||||
return-every-protocol-url-vless-vmess-trojan-ss-hysteria-hy2-for-clients-matching-the-subscription-id-same-result-set-as-subsubid-but-as-a-json-array--no-base64-when-an-inbound-has-streamsettingsexternalproxy-set-one-url-is-emitted-per-external-proxy-empty-array-when-the-subid-has-no-enabled-clients
|
||||
- content: >-
|
||||
Return every URL for one client across all attached inbounds — the
|
||||
id: return-every-protocol-url-vless-vmess-trojan-ss-hysteria-hy2-for-clients-matching-the-subscription-id-same-result-set-as-subsubid-but-as-a-json-array--no-base64-when-an-inbound-has-streamsettingsexternalproxy-set-one-url-is-emitted-per-external-proxy-empty-array-when-the-subid-has-no-enabled-clients
|
||||
- content: 'Return every URL for one client across all attached inbounds — the
|
||||
same strings the Copy URL button copies in the panel UI. Supported
|
||||
protocols: vmess, vless, trojan, shadowsocks, hysteria. If
|
||||
streamSettings.externalProxy is set, returns one URL per external
|
||||
proxy. Protocols without a URL form (socks, http, mixed, wireguard,
|
||||
dokodemo, tunnel) contribute nothing.
|
||||
id: >-
|
||||
return-every-url-for-one-client-across-all-attached-inbounds--the-same-strings-the-copy-url-button-copies-in-the-panel-ui-supported-protocols-vmess-vless-trojan-shadowsocks-hysteria-if-streamsettingsexternalproxy-is-set-returns-one-url-per-external-proxy-protocols-without-a-url-form-socks-http-mixed-wireguard-dokodemo-tunnel-contribute-nothing
|
||||
contents: []
|
||||
dokodemo, tunnel) contribute nothing.'
|
||||
id: return-every-url-for-one-client-across-all-attached-inbounds--the-same-strings-the-copy-url-button-copies-in-the-panel-ui-supported-protocols-vmess-vless-trojan-shadowsocks-hysteria-if-streamsettingsexternalproxy-is-set-returns-one-url-per-external-proxy-protocols-without-a-url-form-socks-http-mixed-wireguard-dokodemo-tunnel-contribute-nothing
|
||||
contents:
|
||||
- content: >-
|
||||
Fields the server fills in when they are omitted — a valid value sent
|
||||
by the caller is never overwritten. Re-adding an email that already
|
||||
exists, with its stored `subId`, reuses the stored `id`, `password`,
|
||||
`auth` and `secret` instead of minting new ones, so the identity stays
|
||||
in sync across its inbounds.
|
||||
|
||||
|
||||
- **VLESS / VMess** — `id`, a fresh UUID
|
||||
|
||||
- **Trojan** — `password`
|
||||
|
||||
- **Shadowsocks** — `password`. On a `2022-blake3-*` inbound a
|
||||
supplied password that does not base64-decode to the key length of the
|
||||
cipher (16 or 32 bytes) is replaced by a generated key and the call
|
||||
still succeeds, so read the client back if you did not let the server
|
||||
pick. Legacy ciphers keep any non-empty password
|
||||
|
||||
- **Hysteria** — `auth`
|
||||
|
||||
- **mtproto** — `secret`, a FakeTLS secret derived from the fronting
|
||||
domain of the inbound, or from `www.cloudflare.com` when it has none
|
||||
|
||||
- **WireGuard** — `privateKey` and `publicKey` when both are blank, or
|
||||
`publicKey` alone when only a `privateKey` was sent, plus
|
||||
`allowedIPs`: one free `/32` taken from the /24 the existing peers of
|
||||
that inbound already sit in, or from `10.0.0.0/24` when it has none
|
||||
|
||||
|
||||
Accepted on the same body but never generated: `preSharedKey` and
|
||||
`keepAlive` (WireGuard), `adTag` (mtproto).
|
||||
|
||||
|
||||
WireGuard is the only one of these that can fail. Allocation widens
|
||||
the search to the containing /16 before giving up with `wireguard: no
|
||||
free address available in <scope>`, and an `allowedIPs` supplied by
|
||||
the caller is validated instead of allocated: `wireguard: allowedIPs
|
||||
entry already used by another client: <address>` when a different
|
||||
client of that same inbound already holds it. The check is per
|
||||
inbound, so the same address on two different inbounds is accepted.
|
||||
The same validation runs on POST /panel/api/clients/{email}/attach,
|
||||
where a client that already carries an address brings it along.
|
||||
heading: create-a-new-client-and-attach-it-to-one-or-more-inbounds-in-a-single-call-body-is-json-per-protocol-secrets-are-generated-server-side-when-omitted-so-callers-can-send-only-the-universal-fields
|
||||
- content: 'A WireGuard client brings its stored `allowedIPs` into the new inbound
|
||||
instead of being given a fresh address, so the call fails with
|
||||
`wireguard: allowedIPs entry already used by another client:
|
||||
<address>` when a different client of the target inbound already holds
|
||||
it. Free the address on that inbound first — see POST
|
||||
/panel/api/clients/add for the full rule.'
|
||||
heading: attach-an-existing-client-to-one-or-more-additional-inbounds-body-is-json
|
||||
---
|
||||
|
||||
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
|
||||
@@ -621,7 +569,7 @@ export default function Layout(props) {
|
||||
return (
|
||||
<>
|
||||
{props.children}
|
||||
<Comp document="./public/openapi.json" webhooks={[]} operations={[{"path":"/panel/api/clients/list","method":"get"},{"path":"/panel/api/clients/list/paged","method":"get"},{"path":"/panel/api/clients/get/{email}","method":"get"},{"path":"/panel/api/clients/add","method":"post"},{"path":"/panel/api/clients/update/{email}","method":"post"},{"path":"/panel/api/clients/del/{email}","method":"post"},{"path":"/panel/api/clients/{email}/attach","method":"post"},{"path":"/panel/api/clients/{email}/detach","method":"post"},{"path":"/panel/api/clients/{email}/externalLinks","method":"post"},{"path":"/panel/api/clients/resetAllTraffics","method":"post"},{"path":"/panel/api/clients/delDepleted","method":"post"},{"path":"/panel/api/clients/delOrphans","method":"post"},{"path":"/panel/api/clients/export","method":"get"},{"path":"/panel/api/clients/import","method":"post"},{"path":"/panel/api/clients/bulkAdjust","method":"post"},{"path":"/panel/api/clients/bulkEnable","method":"post"},{"path":"/panel/api/clients/bulkDisable","method":"post"},{"path":"/panel/api/clients/bulkDel","method":"post"},{"path":"/panel/api/clients/bulkCreate","method":"post"},{"path":"/panel/api/clients/groups/bulkAdd","method":"post"},{"path":"/panel/api/clients/groups/bulkRemove","method":"post"},{"path":"/panel/api/clients/bulkAttach","method":"post"},{"path":"/panel/api/clients/bulkDetach","method":"post"},{"path":"/panel/api/clients/bulkResetTraffic","method":"post"},{"path":"/panel/api/clients/groups","method":"get"},{"path":"/panel/api/clients/groups/{name}/emails","method":"get"},{"path":"/panel/api/clients/groups/create","method":"post"},{"path":"/panel/api/clients/groups/rename","method":"post"},{"path":"/panel/api/clients/groups/delete","method":"post"},{"path":"/panel/api/clients/resetTraffic/{email}","method":"post"},{"path":"/panel/api/clients/updateTraffic/{email}","method":"post"},{"path":"/panel/api/clients/ips/{email}","method":"post"},{"path":"/panel/api/clients/clearIps/{email}","method":"post"},{"path":"/panel/api/clients/onlines","method":"post"},{"path":"/panel/api/clients/onlinesByGuid","method":"post"},{"path":"/panel/api/clients/clientIpsByGuid","method":"post"},{"path":"/panel/api/clients/activeInbounds","method":"post"},{"path":"/panel/api/clients/lastOnline","method":"post"},{"path":"/panel/api/clients/traffic/{email}","method":"get"},{"path":"/panel/api/clients/subLinks/{subId}","method":"get"},{"path":"/panel/api/clients/links/{email}","method":"get"}]} showTitle />
|
||||
<Comp document="./public/openapi.json" webhooks={[]} operations={[{"path":"/panel/api/clients/list","method":"get"},{"path":"/panel/api/clients/list/paged","method":"get"},{"path":"/panel/api/clients/get/{email}","method":"get"},{"path":"/panel/api/clients/get/tgId/{tgId}","method":"get"},{"path":"/panel/api/clients/add","method":"post"},{"path":"/panel/api/clients/update/{email}","method":"post"},{"path":"/panel/api/clients/del/{email}","method":"post"},{"path":"/panel/api/clients/{email}/attach","method":"post"},{"path":"/panel/api/clients/{email}/detach","method":"post"},{"path":"/panel/api/clients/{email}/externalLinks","method":"post"},{"path":"/panel/api/clients/resetAllTraffics","method":"post"},{"path":"/panel/api/clients/delDepleted","method":"post"},{"path":"/panel/api/clients/delOrphans","method":"post"},{"path":"/panel/api/clients/export","method":"get"},{"path":"/panel/api/clients/import","method":"post"},{"path":"/panel/api/clients/bulkAdjust","method":"post"},{"path":"/panel/api/clients/bulkEnable","method":"post"},{"path":"/panel/api/clients/bulkDisable","method":"post"},{"path":"/panel/api/clients/bulkDel","method":"post"},{"path":"/panel/api/clients/bulkCreate","method":"post"},{"path":"/panel/api/clients/groups/bulkAdd","method":"post"},{"path":"/panel/api/clients/groups/bulkRemove","method":"post"},{"path":"/panel/api/clients/bulkAttach","method":"post"},{"path":"/panel/api/clients/bulkDetach","method":"post"},{"path":"/panel/api/clients/bulkResetTraffic","method":"post"},{"path":"/panel/api/clients/groups","method":"get"},{"path":"/panel/api/clients/groups/{name}/emails","method":"get"},{"path":"/panel/api/clients/groups/create","method":"post"},{"path":"/panel/api/clients/groups/rename","method":"post"},{"path":"/panel/api/clients/groups/delete","method":"post"},{"path":"/panel/api/clients/groups/resetTraffic","method":"post"},{"path":"/panel/api/clients/resetTraffic/{email}","method":"post"},{"path":"/panel/api/clients/updateTraffic/{email}","method":"post"},{"path":"/panel/api/clients/ips/{email}","method":"post"},{"path":"/panel/api/clients/clearIps/{email}","method":"post"},{"path":"/panel/api/clients/hwids/{email}","method":"post"},{"path":"/panel/api/clients/hwids/{email}","method":"delete"},{"path":"/panel/api/clients/hwids/{email}/{id}","method":"delete"},{"path":"/panel/api/clients/onlines","method":"post"},{"path":"/panel/api/clients/onlinesByGuid","method":"post"},{"path":"/panel/api/clients/clientIpsByGuid","method":"post"},{"path":"/panel/api/clients/activeInbounds","method":"post"},{"path":"/panel/api/clients/lastOnline","method":"post"},{"path":"/panel/api/clients/traffic/{email}","method":"get"},{"path":"/panel/api/clients/subLinks/{subId}","method":"get"},{"path":"/panel/api/clients/links/{email}","method":"get"}]} showTitle />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
---
|
||||
title: Hosts
|
||||
description: >-
|
||||
Per-inbound override endpoints. Each enabled host renders one extra
|
||||
description: Per-inbound override endpoints. Each enabled host renders one extra
|
||||
subscription link/proxy with its own address/port/TLS, superseding the legacy
|
||||
externalProxy array. All endpoints under /panel/api/hosts.
|
||||
full: true
|
||||
@@ -10,86 +9,69 @@ _openapi:
|
||||
- ./public/openapi.json
|
||||
toc:
|
||||
- depth: 2
|
||||
title: >-
|
||||
List every host across all inbounds, grouped by inbound then ordered by
|
||||
title: List every host across all inbounds, grouped by inbound then ordered by
|
||||
sort order.
|
||||
url: >-
|
||||
#list-every-host-across-all-inbounds-grouped-by-inbound-then-ordered-by-sort-order
|
||||
url: '#list-every-host-across-all-inbounds-grouped-by-inbound-then-ordered-by-sort-order'
|
||||
- depth: 2
|
||||
title: Fetch a single host by ID.
|
||||
url: '#fetch-a-single-host-by-id'
|
||||
title: Fetch a single host group by Group ID.
|
||||
url: '#fetch-a-single-host-group-by-group-id'
|
||||
- depth: 2
|
||||
title: Fetch one inbound's hosts, ordered by sort order then id.
|
||||
url: '#fetch-one-inbounds-hosts-ordered-by-sort-order-then-id'
|
||||
title: Fetch one inbound's hosts, grouped by host group.
|
||||
url: '#fetch-one-inbounds-hosts-grouped-by-host-group'
|
||||
- depth: 2
|
||||
title: Distinct, sorted set of tags used across all hosts.
|
||||
url: '#distinct-sorted-set-of-tags-used-across-all-hosts'
|
||||
- depth: 2
|
||||
title: >-
|
||||
Create a host on an inbound. inboundId and remark are required; security
|
||||
defaults to "same" (inherit the inbound).
|
||||
url: >-
|
||||
#create-a-host-on-an-inbound-inboundid-and-remark-are-required-security-defaults-to-same-inherit-the-inbound
|
||||
title: Create a host group on inbounds.
|
||||
url: '#create-a-host-group-on-inbounds'
|
||||
- depth: 2
|
||||
title: >-
|
||||
Replace a host’s content. The inbound and sort order are immutable here
|
||||
(use /reorder for ordering).
|
||||
url: >-
|
||||
#replace-a-hosts-content-the-inbound-and-sort-order-are-immutable-here-use-reorder-for-ordering
|
||||
title: Replace a host group’s content.
|
||||
url: '#replace-a-host-groups-content'
|
||||
- depth: 2
|
||||
title: Delete a host.
|
||||
url: '#delete-a-host'
|
||||
title: Delete a host group.
|
||||
url: '#delete-a-host-group'
|
||||
- depth: 2
|
||||
title: >-
|
||||
Enable or disable a single host (disabled hosts are skipped in
|
||||
subscriptions).
|
||||
url: >-
|
||||
#enable-or-disable-a-single-host-disabled-hosts-are-skipped-in-subscriptions
|
||||
title: Enable or disable a host group.
|
||||
url: '#enable-or-disable-a-host-group'
|
||||
- depth: 2
|
||||
title: Set host sort order by the position of each id in the array.
|
||||
url: '#set-host-sort-order-by-the-position-of-each-id-in-the-array'
|
||||
title: Set host group sort order by the position of each groupId in the array.
|
||||
url: '#set-host-group-sort-order-by-the-position-of-each-groupid-in-the-array'
|
||||
- depth: 2
|
||||
title: Enable or disable many hosts in one call.
|
||||
url: '#enable-or-disable-many-hosts-in-one-call'
|
||||
title: Add a host group to inbounds (same as /add).
|
||||
url: '#add-a-host-group-to-inbounds-same-as-add'
|
||||
- depth: 2
|
||||
title: Delete many hosts in one call.
|
||||
url: '#delete-many-hosts-in-one-call'
|
||||
title: Enable or disable many host groups in one call.
|
||||
url: '#enable-or-disable-many-host-groups-in-one-call'
|
||||
- depth: 2
|
||||
title: Delete many host groups in one call.
|
||||
url: '#delete-many-host-groups-in-one-call'
|
||||
structuredData:
|
||||
headings:
|
||||
- content: >-
|
||||
List every host across all inbounds, grouped by inbound then ordered
|
||||
by sort order.
|
||||
id: >-
|
||||
list-every-host-across-all-inbounds-grouped-by-inbound-then-ordered-by-sort-order
|
||||
- content: Fetch a single host by ID.
|
||||
id: fetch-a-single-host-by-id
|
||||
- content: Fetch one inbound's hosts, ordered by sort order then id.
|
||||
id: fetch-one-inbounds-hosts-ordered-by-sort-order-then-id
|
||||
- content: List every host across all inbounds, grouped by inbound then ordered by
|
||||
sort order.
|
||||
id: list-every-host-across-all-inbounds-grouped-by-inbound-then-ordered-by-sort-order
|
||||
- content: Fetch a single host group by Group ID.
|
||||
id: fetch-a-single-host-group-by-group-id
|
||||
- content: Fetch one inbound's hosts, grouped by host group.
|
||||
id: fetch-one-inbounds-hosts-grouped-by-host-group
|
||||
- content: Distinct, sorted set of tags used across all hosts.
|
||||
id: distinct-sorted-set-of-tags-used-across-all-hosts
|
||||
- content: >-
|
||||
Create a host on an inbound. inboundId and remark are required;
|
||||
security defaults to "same" (inherit the inbound).
|
||||
id: >-
|
||||
create-a-host-on-an-inbound-inboundid-and-remark-are-required-security-defaults-to-same-inherit-the-inbound
|
||||
- content: >-
|
||||
Replace a host’s content. The inbound and sort order are immutable
|
||||
here (use /reorder for ordering).
|
||||
id: >-
|
||||
replace-a-hosts-content-the-inbound-and-sort-order-are-immutable-here-use-reorder-for-ordering
|
||||
- content: Delete a host.
|
||||
id: delete-a-host
|
||||
- content: >-
|
||||
Enable or disable a single host (disabled hosts are skipped in
|
||||
subscriptions).
|
||||
id: >-
|
||||
enable-or-disable-a-single-host-disabled-hosts-are-skipped-in-subscriptions
|
||||
- content: Set host sort order by the position of each id in the array.
|
||||
id: set-host-sort-order-by-the-position-of-each-id-in-the-array
|
||||
- content: Enable or disable many hosts in one call.
|
||||
id: enable-or-disable-many-hosts-in-one-call
|
||||
- content: Delete many hosts in one call.
|
||||
id: delete-many-hosts-in-one-call
|
||||
- content: Create a host group on inbounds.
|
||||
id: create-a-host-group-on-inbounds
|
||||
- content: Replace a host group’s content.
|
||||
id: replace-a-host-groups-content
|
||||
- content: Delete a host group.
|
||||
id: delete-a-host-group
|
||||
- content: Enable or disable a host group.
|
||||
id: enable-or-disable-a-host-group
|
||||
- content: Set host group sort order by the position of each groupId in the array.
|
||||
id: set-host-group-sort-order-by-the-position-of-each-groupid-in-the-array
|
||||
- content: Add a host group to inbounds (same as /add).
|
||||
id: add-a-host-group-to-inbounds-same-as-add
|
||||
- content: Enable or disable many host groups in one call.
|
||||
id: enable-or-disable-many-host-groups-in-one-call
|
||||
- content: Delete many host groups in one call.
|
||||
id: delete-many-host-groups-in-one-call
|
||||
contents: []
|
||||
---
|
||||
|
||||
@@ -102,7 +84,7 @@ export default function Layout(props) {
|
||||
return (
|
||||
<>
|
||||
{props.children}
|
||||
<Comp document="./public/openapi.json" webhooks={[]} operations={[{"path":"/panel/api/hosts/list","method":"get"},{"path":"/panel/api/hosts/get/{id}","method":"get"},{"path":"/panel/api/hosts/byInbound/{inboundId}","method":"get"},{"path":"/panel/api/hosts/tags","method":"get"},{"path":"/panel/api/hosts/add","method":"post"},{"path":"/panel/api/hosts/update/{id}","method":"post"},{"path":"/panel/api/hosts/del/{id}","method":"post"},{"path":"/panel/api/hosts/setEnable/{id}","method":"post"},{"path":"/panel/api/hosts/reorder","method":"post"},{"path":"/panel/api/hosts/bulk/setEnable","method":"post"},{"path":"/panel/api/hosts/bulk/del","method":"post"}]} showTitle />
|
||||
<Comp document="./public/openapi.json" webhooks={[]} operations={[{"path":"/panel/api/hosts/list","method":"get"},{"path":"/panel/api/hosts/get/{groupId}","method":"get"},{"path":"/panel/api/hosts/byInbound/{inboundId}","method":"get"},{"path":"/panel/api/hosts/tags","method":"get"},{"path":"/panel/api/hosts/add","method":"post"},{"path":"/panel/api/hosts/update/{groupId}","method":"post"},{"path":"/panel/api/hosts/del/{groupId}","method":"post"},{"path":"/panel/api/hosts/setEnable/{groupId}","method":"post"},{"path":"/panel/api/hosts/reorder","method":"post"},{"path":"/panel/api/hosts/bulk/add","method":"post"},{"path":"/panel/api/hosts/bulk/setEnable","method":"post"},{"path":"/panel/api/hosts/bulk/del","method":"post"}]} showTitle />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -33,6 +33,15 @@ _openapi:
|
||||
clientStats so the payload stays small even on panels with thousands of
|
||||
clients.
|
||||
url: '#lightweight-picker-projection-of-the-authenticated-users-inbounds-returns-id-remark-tag-protocol-port-a-server-computed-tlsflowcapable-flag-true-for-vless-on-tcp-with-tls-or-reality-or-on-xhttp-with-vless-encryption--vlessenc-enabled-and-ssmethod-the-shadowsocks-cipher-empty-for-non-shadowsocks-inbounds--used-by-the-client-ui-to-generate-a-valid-shadowsocks-2022-psk-use-this-for-dropdowns-and-attach-pickers--it-skips-settings-streamsettings-and-clientstats-so-the-payload-stays-small-even-on-panels-with-thousands-of-clients'
|
||||
- depth: 2
|
||||
title: Return every protocol URL (vless://, vmess://, trojan://, ss://,
|
||||
hysteria://, mtproto) across all inbounds and all of their clients.
|
||||
Links are rendered through the subscription engine, so the configured
|
||||
remark template (name-only display part) is applied per client — the
|
||||
same output the client info/QR pages use. Protocols without a URL form
|
||||
(socks, http, mixed, wireguard, dokodemo, tunnel) contribute nothing.
|
||||
Used by the panel’s "Export all inbound links" action.
|
||||
url: '#return-every-protocol-url-vless-vmess-trojan-ss-hysteria-mtproto-across-all-inbounds-and-all-of-their-clients-links-are-rendered-through-the-subscription-engine-so-the-configured-remark-template-name-only-display-part-is-applied-per-client--the-same-output-the-client-infoqr-pages-use-protocols-without-a-url-form-socks-http-mixed-wireguard-dokodemo-tunnel-contribute-nothing-used-by-the-panels-export-all-inbound-links-action'
|
||||
- depth: 2
|
||||
title: Fetch a single inbound by numeric ID.
|
||||
url: '#fetch-a-single-inbound-by-numeric-id'
|
||||
@@ -59,6 +68,10 @@ _openapi:
|
||||
title: Toggle only the enable flag without serialising the whole settings JSON.
|
||||
Recommended for UI switches on large inbounds.
|
||||
url: '#toggle-only-the-enable-flag-without-serialising-the-whole-settings-json-recommended-for-ui-switches-on-large-inbounds'
|
||||
- depth: 2
|
||||
title: Set only the subscription sort order. Reads the stored inbound, so a
|
||||
reorder cannot carry a stale client list over a concurrent edit.
|
||||
url: '#set-only-the-subscription-sort-order-reads-the-stored-inbound-so-a-reorder-cannot-carry-a-stale-client-list-over-a-concurrent-edit'
|
||||
- depth: 2
|
||||
title: Zero out upload + download counters for a single inbound. Does not touch
|
||||
per-client counters.
|
||||
@@ -94,10 +107,6 @@ _openapi:
|
||||
title: Replace the entire fallback list for a master inbound. Body is JSON.
|
||||
Triggers an Xray restart.
|
||||
url: '#replace-the-entire-fallback-list-for-a-master-inbound-body-is-json-triggers-an-xray-restart'
|
||||
- depth: 2
|
||||
title: Set only the subscription sort order. Reads the stored inbound, so a
|
||||
reorder cannot carry a stale client list over a concurrent edit.
|
||||
url: '#set-only-the-subscription-sort-order-reads-the-stored-inbound-so-a-reorder-cannot-carry-a-stale-client-list-over-a-concurrent-edit'
|
||||
structuredData:
|
||||
headings:
|
||||
- content: List every inbound owned by the authenticated user, including each
|
||||
@@ -121,6 +130,14 @@ _openapi:
|
||||
clientStats so the payload stays small even on panels with thousands
|
||||
of clients.
|
||||
id: lightweight-picker-projection-of-the-authenticated-users-inbounds-returns-id-remark-tag-protocol-port-a-server-computed-tlsflowcapable-flag-true-for-vless-on-tcp-with-tls-or-reality-or-on-xhttp-with-vless-encryption--vlessenc-enabled-and-ssmethod-the-shadowsocks-cipher-empty-for-non-shadowsocks-inbounds--used-by-the-client-ui-to-generate-a-valid-shadowsocks-2022-psk-use-this-for-dropdowns-and-attach-pickers--it-skips-settings-streamsettings-and-clientstats-so-the-payload-stays-small-even-on-panels-with-thousands-of-clients
|
||||
- content: Return every protocol URL (vless://, vmess://, trojan://, ss://,
|
||||
hysteria://, mtproto) across all inbounds and all of their clients.
|
||||
Links are rendered through the subscription engine, so the configured
|
||||
remark template (name-only display part) is applied per client — the
|
||||
same output the client info/QR pages use. Protocols without a URL form
|
||||
(socks, http, mixed, wireguard, dokodemo, tunnel) contribute nothing.
|
||||
Used by the panel’s "Export all inbound links" action.
|
||||
id: return-every-protocol-url-vless-vmess-trojan-ss-hysteria-mtproto-across-all-inbounds-and-all-of-their-clients-links-are-rendered-through-the-subscription-engine-so-the-configured-remark-template-name-only-display-part-is-applied-per-client--the-same-output-the-client-infoqr-pages-use-protocols-without-a-url-form-socks-http-mixed-wireguard-dokodemo-tunnel-contribute-nothing-used-by-the-panels-export-all-inbound-links-action
|
||||
- content: Fetch a single inbound by numeric ID.
|
||||
id: fetch-a-single-inbound-by-numeric-id
|
||||
- content: Create a new inbound. Send the full inbound payload (protocol, port,
|
||||
@@ -141,6 +158,9 @@ _openapi:
|
||||
- content: Toggle only the enable flag without serialising the whole settings
|
||||
JSON. Recommended for UI switches on large inbounds.
|
||||
id: toggle-only-the-enable-flag-without-serialising-the-whole-settings-json-recommended-for-ui-switches-on-large-inbounds
|
||||
- content: Set only the subscription sort order. Reads the stored inbound, so a
|
||||
reorder cannot carry a stale client list over a concurrent edit.
|
||||
id: set-only-the-subscription-sort-order-reads-the-stored-inbound-so-a-reorder-cannot-carry-a-stale-client-list-over-a-concurrent-edit
|
||||
- content: Zero out upload + download counters for a single inbound. Does not
|
||||
touch per-client counters.
|
||||
id: zero-out-upload--download-counters-for-a-single-inbound-does-not-touch-per-client-counters
|
||||
@@ -169,9 +189,6 @@ _openapi:
|
||||
- content: Replace the entire fallback list for a master inbound. Body is JSON.
|
||||
Triggers an Xray restart.
|
||||
id: replace-the-entire-fallback-list-for-a-master-inbound-body-is-json-triggers-an-xray-restart
|
||||
- content: Set only the subscription sort order. Reads the stored inbound, so a
|
||||
reorder cannot carry a stale client list over a concurrent edit.
|
||||
id: set-only-the-subscription-sort-order-reads-the-stored-inbound-so-a-reorder-cannot-carry-a-stale-client-list-over-a-concurrent-edit
|
||||
contents: []
|
||||
---
|
||||
|
||||
@@ -184,7 +201,7 @@ export default function Layout(props) {
|
||||
return (
|
||||
<>
|
||||
{props.children}
|
||||
<Comp document="./public/openapi.json" webhooks={[]} operations={[{"path":"/panel/api/inbounds/list","method":"get"},{"path":"/panel/api/inbounds/list/slim","method":"get"},{"path":"/panel/api/inbounds/options","method":"get"},{"path":"/panel/api/inbounds/get/{id}","method":"get"},{"path":"/panel/api/inbounds/add","method":"post"},{"path":"/panel/api/inbounds/del/{id}","method":"post"},{"path":"/panel/api/inbounds/bulkDel","method":"post"},{"path":"/panel/api/inbounds/update/{id}","method":"post"},{"path":"/panel/api/inbounds/setEnable/{id}","method":"post"},{"path":"/panel/api/inbounds/{id}/resetTraffic","method":"post"},{"path":"/panel/api/inbounds/{id}/delAllClients","method":"post"},{"path":"/panel/api/inbounds/resetAllTraffics","method":"post"},{"path":"/panel/api/inbounds/import","method":"post"},{"path":"/panel/api/inbounds/pushClientTraffics","method":"post"},{"path":"/panel/api/inbounds/{id}/fallbacks","method":"get"},{"path":"/panel/api/inbounds/{id}/fallbacks","method":"post"},{"path":"/panel/api/inbounds/{id}/subSortIndex","method":"post"}]} showTitle />
|
||||
<Comp document="./public/openapi.json" webhooks={[]} operations={[{"path":"/panel/api/inbounds/list","method":"get"},{"path":"/panel/api/inbounds/list/slim","method":"get"},{"path":"/panel/api/inbounds/options","method":"get"},{"path":"/panel/api/inbounds/allLinks","method":"get"},{"path":"/panel/api/inbounds/get/{id}","method":"get"},{"path":"/panel/api/inbounds/add","method":"post"},{"path":"/panel/api/inbounds/del/{id}","method":"post"},{"path":"/panel/api/inbounds/bulkDel","method":"post"},{"path":"/panel/api/inbounds/update/{id}","method":"post"},{"path":"/panel/api/inbounds/setEnable/{id}","method":"post"},{"path":"/panel/api/inbounds/{id}/subSortIndex","method":"post"},{"path":"/panel/api/inbounds/{id}/resetTraffic","method":"post"},{"path":"/panel/api/inbounds/{id}/delAllClients","method":"post"},{"path":"/panel/api/inbounds/resetAllTraffics","method":"post"},{"path":"/panel/api/inbounds/import","method":"post"},{"path":"/panel/api/inbounds/pushClientTraffics","method":"post"},{"path":"/panel/api/inbounds/{id}/fallbacks","method":"get"},{"path":"/panel/api/inbounds/{id}/fallbacks","method":"post"}]} showTitle />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -11,6 +11,7 @@
|
||||
"settings",
|
||||
"xray-settings",
|
||||
"subscription-server",
|
||||
"subscription-balancers",
|
||||
"hosts",
|
||||
"nodes",
|
||||
"backup",
|
||||
|
||||
@@ -22,6 +22,11 @@ _openapi:
|
||||
CA (from nodes/mtls/ca). An empty caCert disables it. A non-empty value
|
||||
must be a PEM certificate. Applied on the next panel restart.
|
||||
url: '#set-the-ca-certificate-this-panel-trusts-for-incoming-node-api-client-certificates-this-panel-acting-as-a-node-paste-the-managing-panels-ca-from-nodesmtlsca-an-empty-cacert-disables-it-a-non-empty-value-must-be-a-pem-certificate-applied-on-the-next-panel-restart'
|
||||
- depth: 2
|
||||
title: Validate the stored master mTLS client credential and invalidate cached
|
||||
transports. Each transport closes its old idle pool and rebuilds with
|
||||
the rotated certificate before its next request.
|
||||
url: '#validate-the-stored-master-mtls-client-credential-and-invalidate-cached-transports-each-transport-closes-its-old-idle-pool-and-rebuilds-with-the-rotated-certificate-before-its-next-request'
|
||||
- depth: 2
|
||||
title: Fetch a single node by ID.
|
||||
url: '#fetch-a-single-node-by-id'
|
||||
@@ -32,12 +37,15 @@ _openapi:
|
||||
panel.
|
||||
url: '#fetch-a-nodes-own-web-tls-certificatekey-file-paths-proxied-to-the-node-used-by-the-inbound-forms-set-cert-from-panel-so-a-node-assigned-inbound-gets-paths-that-exist-on-the-node-not-the-central-panel'
|
||||
- depth: 2
|
||||
title: Register a new remote node. Provide its URL, apiToken, and optional
|
||||
remark / allowPrivateAddress flag.
|
||||
url: '#register-a-new-remote-node-provide-its-url-apitoken-and-optional-remark--allowprivateaddress-flag'
|
||||
title: Register a new remote node. Provide its URL, write-only apiToken, and
|
||||
optional remark / allowPrivateAddress flag. Responses expose hasApiToken
|
||||
only.
|
||||
url: '#register-a-new-remote-node-provide-its-url-write-only-apitoken-and-optional-remark--allowprivateaddress-flag-responses-expose-hasapitoken-only'
|
||||
- depth: 2
|
||||
title: Replace a node’s connection details. Same body shape as /add.
|
||||
url: '#replace-a-nodes-connection-details-same-body-shape-as-add'
|
||||
title: 'Replace a node’s connection details. apiToken is write-only: omit it or
|
||||
send an empty string to keep the stored token; set clearApiToken=true to
|
||||
clear it.'
|
||||
url: '#replace-a-nodes-connection-details-apitoken-is-write-only-omit-it-or-send-an-empty-string-to-keep-the-stored-token-set-clearapitokentrue-to-clear-it'
|
||||
- depth: 2
|
||||
title: Delete a node. Inbounds bound to it are not auto-migrated.
|
||||
url: '#delete-a-node-inbounds-bound-to-it-are-not-auto-migrated'
|
||||
@@ -72,11 +80,6 @@ _openapi:
|
||||
title: Aggregated metric history for a node — same shape as /server/history,
|
||||
scoped to one node.
|
||||
url: '#aggregated-metric-history-for-a-node--same-shape-as-serverhistory-scoped-to-one-node'
|
||||
- depth: 2
|
||||
title: Validate the stored master mTLS client credential and invalidate cached
|
||||
transports. Each transport closes its old idle pool and rebuilds with
|
||||
the rotated certificate before its next request.
|
||||
url: '#validate-the-stored-master-mtls-client-credential-and-invalidate-cached-transports-each-transport-closes-its-old-idle-pool-and-rebuilds-with-the-rotated-certificate-before-its-next-request'
|
||||
structuredData:
|
||||
headings:
|
||||
- content: List every configured node with its connection details, health, and
|
||||
@@ -91,6 +94,10 @@ _openapi:
|
||||
CA (from nodes/mtls/ca). An empty caCert disables it. A non-empty
|
||||
value must be a PEM certificate. Applied on the next panel restart.
|
||||
id: set-the-ca-certificate-this-panel-trusts-for-incoming-node-api-client-certificates-this-panel-acting-as-a-node-paste-the-managing-panels-ca-from-nodesmtlsca-an-empty-cacert-disables-it-a-non-empty-value-must-be-a-pem-certificate-applied-on-the-next-panel-restart
|
||||
- content: Validate the stored master mTLS client credential and invalidate cached
|
||||
transports. Each transport closes its old idle pool and rebuilds with
|
||||
the rotated certificate before its next request.
|
||||
id: validate-the-stored-master-mtls-client-credential-and-invalidate-cached-transports-each-transport-closes-its-old-idle-pool-and-rebuilds-with-the-rotated-certificate-before-its-next-request
|
||||
- content: Fetch a single node by ID.
|
||||
id: fetch-a-single-node-by-id
|
||||
- content: Fetch a node's own web TLS certificate/key file paths (proxied to the
|
||||
@@ -98,11 +105,14 @@ _openapi:
|
||||
node-assigned inbound gets paths that exist on the node, not the
|
||||
central panel.
|
||||
id: fetch-a-nodes-own-web-tls-certificatekey-file-paths-proxied-to-the-node-used-by-the-inbound-forms-set-cert-from-panel-so-a-node-assigned-inbound-gets-paths-that-exist-on-the-node-not-the-central-panel
|
||||
- content: Register a new remote node. Provide its URL, apiToken, and optional
|
||||
remark / allowPrivateAddress flag.
|
||||
id: register-a-new-remote-node-provide-its-url-apitoken-and-optional-remark--allowprivateaddress-flag
|
||||
- content: Replace a node’s connection details. Same body shape as /add.
|
||||
id: replace-a-nodes-connection-details-same-body-shape-as-add
|
||||
- content: Register a new remote node. Provide its URL, write-only apiToken, and
|
||||
optional remark / allowPrivateAddress flag. Responses expose
|
||||
hasApiToken only.
|
||||
id: register-a-new-remote-node-provide-its-url-write-only-apitoken-and-optional-remark--allowprivateaddress-flag-responses-expose-hasapitoken-only
|
||||
- content: 'Replace a node’s connection details. apiToken is write-only: omit it
|
||||
or send an empty string to keep the stored token; set
|
||||
clearApiToken=true to clear it.'
|
||||
id: replace-a-nodes-connection-details-apitoken-is-write-only-omit-it-or-send-an-empty-string-to-keep-the-stored-token-set-clearapitokentrue-to-clear-it
|
||||
- content: Delete a node. Inbounds bound to it are not auto-migrated.
|
||||
id: delete-a-node-inbounds-bound-to-it-are-not-auto-migrated
|
||||
- content: Pause or resume traffic sync with this node.
|
||||
@@ -129,10 +139,6 @@ _openapi:
|
||||
- content: Aggregated metric history for a node — same shape as /server/history,
|
||||
scoped to one node.
|
||||
id: aggregated-metric-history-for-a-node--same-shape-as-serverhistory-scoped-to-one-node
|
||||
- content: Validate the stored master mTLS client credential and invalidate cached
|
||||
transports. Each transport closes its old idle pool and rebuilds with
|
||||
the rotated certificate before its next request.
|
||||
id: validate-the-stored-master-mtls-client-credential-and-invalidate-cached-transports-each-transport-closes-its-old-idle-pool-and-rebuilds-with-the-rotated-certificate-before-its-next-request
|
||||
contents: []
|
||||
---
|
||||
|
||||
@@ -145,7 +151,7 @@ export default function Layout(props) {
|
||||
return (
|
||||
<>
|
||||
{props.children}
|
||||
<Comp document="./public/openapi.json" webhooks={[]} operations={[{"path":"/panel/api/nodes/list","method":"get"},{"path":"/panel/api/nodes/mtls/ca","method":"post"},{"path":"/panel/api/nodes/mtls/trustCA","method":"post"},{"path":"/panel/api/nodes/get/{id}","method":"get"},{"path":"/panel/api/nodes/webCert/{id}","method":"get"},{"path":"/panel/api/nodes/add","method":"post"},{"path":"/panel/api/nodes/update/{id}","method":"post"},{"path":"/panel/api/nodes/del/{id}","method":"post"},{"path":"/panel/api/nodes/setEnable/{id}","method":"post"},{"path":"/panel/api/nodes/test","method":"post"},{"path":"/panel/api/nodes/certFingerprint","method":"post"},{"path":"/panel/api/nodes/inbounds","method":"post"},{"path":"/panel/api/nodes/probe/{id}","method":"post"},{"path":"/panel/api/nodes/updatePanel","method":"post"},{"path":"/panel/api/nodes/history/{id}/{metric}/{bucket}","method":"get"},{"path":"/panel/api/nodes/mtls/reloadClient","method":"post"}]} showTitle />
|
||||
<Comp document="./public/openapi.json" webhooks={[]} operations={[{"path":"/panel/api/nodes/list","method":"get"},{"path":"/panel/api/nodes/mtls/ca","method":"post"},{"path":"/panel/api/nodes/mtls/trustCA","method":"post"},{"path":"/panel/api/nodes/mtls/reloadClient","method":"post"},{"path":"/panel/api/nodes/get/{id}","method":"get"},{"path":"/panel/api/nodes/webCert/{id}","method":"get"},{"path":"/panel/api/nodes/add","method":"post"},{"path":"/panel/api/nodes/update/{id}","method":"post"},{"path":"/panel/api/nodes/del/{id}","method":"post"},{"path":"/panel/api/nodes/setEnable/{id}","method":"post"},{"path":"/panel/api/nodes/test","method":"post"},{"path":"/panel/api/nodes/certFingerprint","method":"post"},{"path":"/panel/api/nodes/inbounds","method":"post"},{"path":"/panel/api/nodes/probe/{id}","method":"post"},{"path":"/panel/api/nodes/updatePanel","method":"post"},{"path":"/panel/api/nodes/history/{id}/{metric}/{bucket}","method":"get"}]} showTitle />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,159 +1,131 @@
|
||||
---
|
||||
title: Server
|
||||
description: >-
|
||||
System status, log retrieval, certificate generators, Xray binary management,
|
||||
and backup/restore. All under /panel/api/server.
|
||||
description: System status, log retrieval, certificate generators, Xray binary
|
||||
management, and backup/restore. All under /panel/api/server.
|
||||
full: true
|
||||
_openapi:
|
||||
preload:
|
||||
- ./public/openapi.json
|
||||
toc:
|
||||
- depth: 2
|
||||
title: >-
|
||||
Real-time machine snapshot: CPU, memory, swap, disk, network IO, load
|
||||
averages, open connections, Xray state. Cached and refreshed every 2
|
||||
seconds in the background.
|
||||
url: >-
|
||||
#real-time-machine-snapshot-cpu-memory-swap-disk-network-io-load-averages-open-connections-xray-state-cached-and-refreshed-every-2-seconds-in-the-background
|
||||
title: Serve this API description as an OpenAPI 3 document — the same file that
|
||||
powers the API Docs page. Requires a session or Bearer token like the
|
||||
rest of /panel/api. Useful for generating clients or importing into API
|
||||
tooling.
|
||||
url: '#serve-this-api-description-as-an-openapi-3-document--the-same-file-that-powers-the-api-docs-page-requires-a-session-or-bearer-token-like-the-rest-of-panelapi-useful-for-generating-clients-or-importing-into-api-tooling'
|
||||
- depth: 2
|
||||
title: >-
|
||||
Reports whether per-client IP limits can be enforced on this host. The
|
||||
title: 'Real-time machine snapshot: CPU, memory, swap, disk, network IO, load
|
||||
averages, open connections, Xray state. Cached and refreshed every 2
|
||||
seconds in the background.'
|
||||
url: '#real-time-machine-snapshot-cpu-memory-swap-disk-network-io-load-averages-open-connections-xray-state-cached-and-refreshed-every-2-seconds-in-the-background'
|
||||
- depth: 2
|
||||
title: Reports whether per-client IP limits can be enforced on this host. The
|
||||
panel uses it to gate the "IP Limit" field, since enforcement depends on
|
||||
Fail2ban being installed.
|
||||
url: >-
|
||||
#reports-whether-per-client-ip-limits-can-be-enforced-on-this-host-the-panel-uses-it-to-gate-the-ip-limit-field-since-enforcement-depends-on-fail2ban-being-installed
|
||||
url: '#reports-whether-per-client-ip-limits-can-be-enforced-on-this-host-the-panel-uses-it-to-gate-the-ip-limit-field-since-enforcement-depends-on-fail2ban-being-installed'
|
||||
- depth: 2
|
||||
title: >-
|
||||
Legacy: aggregated CPU history. Use /history/cpu/:bucket instead — same
|
||||
data with a uniform {t, v} shape.
|
||||
url: >-
|
||||
#legacy-aggregated-cpu-history-use-historycpubucket-instead--same-data-with-a-uniform-t-v-shape
|
||||
title: 'Legacy: aggregated CPU history. Use /history/cpu/:bucket instead — same
|
||||
data with a uniform {t, v} shape.'
|
||||
url: '#legacy-aggregated-cpu-history-use-historycpubucket-instead--same-data-with-a-uniform-t-v-shape'
|
||||
- depth: 2
|
||||
title: >-
|
||||
Aggregated time-series for one metric. Returns an array of {t, v}
|
||||
samples covering the last ~6 hours.
|
||||
url: >-
|
||||
#aggregated-time-series-for-one-metric-returns-an-array-of-t-v-samples-covering-the-last-6-hours
|
||||
title: Aggregated time-series for one metric. Returns an array of {t, v} samples
|
||||
covering the last ~6 hours.
|
||||
url: '#aggregated-time-series-for-one-metric-returns-an-array-of-t-v-samples-covering-the-last-6-hours'
|
||||
- depth: 2
|
||||
title: >-
|
||||
Xray runtime metrics state — whether the xray config has a `metrics`
|
||||
title: Xray runtime metrics state — whether the xray config has a `metrics`
|
||||
block, which expvar keys are flowing, and the current snapshot values
|
||||
for each. Returns an empty state when metrics are not configured.
|
||||
url: >-
|
||||
#xray-runtime-metrics-state--whether-the-xray-config-has-a-metrics-block-which-expvar-keys-are-flowing-and-the-current-snapshot-values-for-each-returns-an-empty-state-when-metrics-are-not-configured
|
||||
url: '#xray-runtime-metrics-state--whether-the-xray-config-has-a-metrics-block-which-expvar-keys-are-flowing-and-the-current-snapshot-values-for-each-returns-an-empty-state-when-metrics-are-not-configured'
|
||||
- depth: 2
|
||||
title: >-
|
||||
Time-series history for one Xray runtime metric over the last ~6 hours.
|
||||
title: Time-series history for one Xray runtime metric over the last ~6 hours.
|
||||
Same {t, v} shape as /history/:metric/:bucket.
|
||||
url: >-
|
||||
#time-series-history-for-one-xray-runtime-metric-over-the-last-6-hours-same-t-v-shape-as-historymetricbucket
|
||||
url: '#time-series-history-for-one-xray-runtime-metric-over-the-last-6-hours-same-t-v-shape-as-historymetricbucket'
|
||||
- depth: 2
|
||||
title: >-
|
||||
Latest snapshot from the Xray observatory — per-outbound latency, health
|
||||
title: Latest snapshot from the Xray observatory — per-outbound latency, health
|
||||
status, and last-probe time. Only populated when the Xray config has an
|
||||
observatory configured.
|
||||
url: >-
|
||||
#latest-snapshot-from-the-xray-observatory--per-outbound-latency-health-status-and-last-probe-time-only-populated-when-the-xray-config-has-an-observatory-configured
|
||||
url: '#latest-snapshot-from-the-xray-observatory--per-outbound-latency-health-status-and-last-probe-time-only-populated-when-the-xray-config-has-an-observatory-configured'
|
||||
- depth: 2
|
||||
title: >-
|
||||
Time-series of observatory probe results for one outbound tag. Same {t,
|
||||
title: Time-series of observatory probe results for one outbound tag. Same {t,
|
||||
v} shape as the other history endpoints.
|
||||
url: >-
|
||||
#time-series-of-observatory-probe-results-for-one-outbound-tag-same-t-v-shape-as-the-other-history-endpoints
|
||||
url: '#time-series-of-observatory-probe-results-for-one-outbound-tag-same-t-v-shape-as-the-other-history-endpoints'
|
||||
- depth: 2
|
||||
title: List Xray binary versions available for install on this host.
|
||||
url: '#list-xray-binary-versions-available-for-install-on-this-host'
|
||||
- depth: 2
|
||||
title: Check whether a newer 3x-ui release is available on GitHub.
|
||||
url: '#check-whether-a-newer-3x-ui-release-is-available-on-github'
|
||||
- depth: 2
|
||||
title: Report the outcome of the most recently launched panel self-update (see
|
||||
POST updatePanel). Compare the returned runId against the one
|
||||
updatePanel returned to tell this run apart from a stale result.
|
||||
url: '#report-the-outcome-of-the-most-recently-launched-panel-self-update-see-post-updatepanel-compare-the-returned-runid-against-the-one-updatepanel-returned-to-tell-this-run-apart-from-a-stale-result'
|
||||
- depth: 2
|
||||
title: Return the assembled Xray config that’s currently running on this host.
|
||||
url: '#return-the-assembled-xray-config-thats-currently-running-on-this-host'
|
||||
- depth: 2
|
||||
title: >-
|
||||
Stream the SQLite database file as an attachment. Use as a manual
|
||||
backup.
|
||||
url: '#stream-the-sqlite-database-file-as-an-attachment-use-as-a-manual-backup'
|
||||
title: 'Stream a full database backup as an attachment: the SQLite .db file on
|
||||
SQLite panels, or a pg_dump custom-format archive (.dump) on PostgreSQL
|
||||
panels. Use as a manual backup.'
|
||||
url: '#stream-a-full-database-backup-as-an-attachment-the-sqlite-db-file-on-sqlite-panels-or-a-pg_dump-custom-format-archive-dump-on-postgresql-panels-use-as-a-manual-backup'
|
||||
- depth: 2
|
||||
title: >-
|
||||
Stream a cross-engine migration file as an attachment: a .dump (SQL
|
||||
title: 'Stream a cross-engine migration file as an attachment: a .dump (SQL
|
||||
text) on SQLite, or a .db SQLite database built from the live data on
|
||||
PostgreSQL.
|
||||
url: >-
|
||||
#stream-a-cross-engine-migration-file-as-an-attachment-a-dump-sql-text-on-sqlite-or-a-db-sqlite-database-built-from-the-live-data-on-postgresql
|
||||
PostgreSQL.'
|
||||
url: '#stream-a-cross-engine-migration-file-as-an-attachment-a-dump-sql-text-on-sqlite-or-a-db-sqlite-database-built-from-the-live-data-on-postgresql'
|
||||
- depth: 2
|
||||
title: Generate a fresh UUID v4. Convenience helper for client IDs.
|
||||
url: '#generate-a-fresh-uuid-v4-convenience-helper-for-client-ids'
|
||||
- depth: 2
|
||||
title: >-
|
||||
Return this panel's own web TLS certificate and key file paths. The
|
||||
title: Return this panel's own web TLS certificate and key file paths. The
|
||||
central panel calls it on a node (via the node API token) so "Set Cert
|
||||
from Panel" fills a node-assigned inbound with paths that exist on the
|
||||
node.
|
||||
url: >-
|
||||
#return-this-panels-own-web-tls-certificate-and-key-file-paths-the-central-panel-calls-it-on-a-node-via-the-node-api-token-so-set-cert-from-panel-fills-a-node-assigned-inbound-with-paths-that-exist-on-the-node
|
||||
url: '#return-this-panels-own-web-tls-certificate-and-key-file-paths-the-central-panel-calls-it-on-a-node-via-the-node-api-token-so-set-cert-from-panel-fills-a-node-assigned-inbound-with-paths-that-exist-on-the-node'
|
||||
- depth: 2
|
||||
title: >-
|
||||
Read-only summaries (guid, parentGuid, name, address, status, versions)
|
||||
title: Read-only summaries (guid, parentGuid, name, address, status, versions)
|
||||
of the nodes this panel manages. A parent panel calls it on a node (via
|
||||
the node API token) to surface transitive sub-nodes in a chained
|
||||
topology. Counts are computed by the parent, not returned here.
|
||||
url: >-
|
||||
#read-only-summaries-guid-parentguid-name-address-status-versions-of-the-nodes-this-panel-manages-a-parent-panel-calls-it-on-a-node-via-the-node-api-token-to-surface-transitive-sub-nodes-in-a-chained-topology-counts-are-computed-by-the-parent-not-returned-here
|
||||
url: '#read-only-summaries-guid-parentguid-name-address-status-versions-of-the-nodes-this-panel-manages-a-parent-panel-calls-it-on-a-node-via-the-node-api-token-to-surface-transitive-sub-nodes-in-a-chained-topology-counts-are-computed-by-the-parent-not-returned-here'
|
||||
- depth: 2
|
||||
title: Generate a new X25519 keypair for Reality.
|
||||
url: '#generate-a-new-x25519-keypair-for-reality'
|
||||
- depth: 2
|
||||
title: >-
|
||||
Generate a new ML-DSA-65 keypair (post-quantum signature). Returns
|
||||
title: Generate a new ML-DSA-65 keypair (post-quantum signature). Returns
|
||||
{privateKey, publicKey, seed}.
|
||||
url: >-
|
||||
#generate-a-new-ml-dsa-65-keypair-post-quantum-signature-returns-privatekey-publickey-seed
|
||||
url: '#generate-a-new-ml-dsa-65-keypair-post-quantum-signature-returns-privatekey-publickey-seed'
|
||||
- depth: 2
|
||||
title: >-
|
||||
Generate a new ML-KEM-768 keypair (post-quantum KEM). Returns
|
||||
{clientKey, serverKey}.
|
||||
url: >-
|
||||
#generate-a-new-ml-kem-768-keypair-post-quantum-kem-returns-clientkey-serverkey
|
||||
title: Generate a new ML-KEM-768 keypair (post-quantum KEM). Returns {clientKey,
|
||||
serverKey}.
|
||||
url: '#generate-a-new-ml-kem-768-keypair-post-quantum-kem-returns-clientkey-serverkey'
|
||||
- depth: 2
|
||||
title: >-
|
||||
Generate VLESS encryption auth options. Returns an auths array each with
|
||||
title: Generate VLESS encryption auth options. Returns an auths array each with
|
||||
id, label, encryption, and decryption fields.
|
||||
url: >-
|
||||
#generate-vless-encryption-auth-options-returns-an-auths-array-each-with-id-label-encryption-and-decryption-fields
|
||||
url: '#generate-vless-encryption-auth-options-returns-an-auths-array-each-with-id-label-encryption-and-decryption-fields'
|
||||
- depth: 2
|
||||
title: Stop the Xray binary. All proxies go offline immediately.
|
||||
url: '#stop-the-xray-binary-all-proxies-go-offline-immediately'
|
||||
- depth: 2
|
||||
title: >-
|
||||
Reload Xray with the current config. Typically required after structural
|
||||
title: Reload Xray with the current config. Typically required after structural
|
||||
inbound or routing changes.
|
||||
url: >-
|
||||
#reload-xray-with-the-current-config-typically-required-after-structural-inbound-or-routing-changes
|
||||
url: '#reload-xray-with-the-current-config-typically-required-after-structural-inbound-or-routing-changes'
|
||||
- depth: 2
|
||||
title: >-
|
||||
Download and install the specified Xray version. Pass "latest" for the
|
||||
title: Download and install the specified Xray version. Pass "latest" for the
|
||||
newest release.
|
||||
url: >-
|
||||
#download-and-install-the-specified-xray-version-pass-latest-for-the-newest-release
|
||||
url: '#download-and-install-the-specified-xray-version-pass-latest-for-the-newest-release'
|
||||
- depth: 2
|
||||
title: >-
|
||||
Self-update the panel to the latest version. The server restarts on
|
||||
title: Self-update the panel to the latest version. The server restarts on
|
||||
success.
|
||||
url: >-
|
||||
#self-update-the-panel-to-the-latest-version-the-server-restarts-on-success
|
||||
url: '#self-update-the-panel-to-the-latest-version-the-server-restarts-on-success'
|
||||
- depth: 2
|
||||
title: >-
|
||||
Toggle the panel update channel between stable and the rolling
|
||||
per-commit dev release. Only effective on dev builds.
|
||||
url: >-
|
||||
#toggle-the-panel-update-channel-between-stable-and-the-rolling-per-commit-dev-release-only-effective-on-dev-builds
|
||||
title: Toggle the panel update channel between stable and the rolling per-commit
|
||||
dev release. Only effective on dev builds.
|
||||
url: '#toggle-the-panel-update-channel-between-stable-and-the-rolling-per-commit-dev-release-only-effective-on-dev-builds'
|
||||
- depth: 2
|
||||
title: >-
|
||||
Refresh the default GeoIP / GeoSite data files. Body can include a
|
||||
title: Refresh the default GeoIP / GeoSite data files. Body can include a
|
||||
fileName, or use the /:fileName variant.
|
||||
url: >-
|
||||
#refresh-the-default-geoip--geosite-data-files-body-can-include-a-filename-or-use-the-filename-variant
|
||||
url: '#refresh-the-default-geoip--geosite-data-files-body-can-include-a-filename-or-use-the-filename-variant'
|
||||
- depth: 2
|
||||
title: Refresh a single Geo file by filename (e.g. geoip.dat, geosite.dat).
|
||||
url: '#refresh-a-single-geo-file-by-filename-eg-geoipdat-geositedat'
|
||||
@@ -164,205 +136,187 @@ _openapi:
|
||||
title: Return the last N lines of the Xray process log.
|
||||
url: '#return-the-last-n-lines-of-the-xray-process-log'
|
||||
- depth: 2
|
||||
title: >-
|
||||
Restore the panel DB from an uploaded SQLite file (multipart form, field
|
||||
name "db"). The panel restarts after restore. Destructive.
|
||||
url: >-
|
||||
#restore-the-panel-db-from-an-uploaded-sqlite-file-multipart-form-field-name-db-the-panel-restarts-after-restore-destructive
|
||||
title: Return live AmneziaWG peer activity (handshake, endpoint, transfer) plus
|
||||
the panel’s own AmneziaWG event lines.
|
||||
url: '#return-live-amneziawg-peer-activity-handshake-endpoint-transfer-plus-the-panels-own-amneziawg-event-lines'
|
||||
- depth: 2
|
||||
title: >-
|
||||
Generate a new ECH (Encrypted Client Hello) keypair and config list for
|
||||
title: Restore the panel DB from an uploaded backup (multipart form, field name
|
||||
"db"). SQLite panels accept a SQLite database (.db) or a SQLite
|
||||
migration dump (.dump); PostgreSQL panels accept a pg_dump archive
|
||||
(.dump), a SQLite database (.db), or a SQLite migration dump. The panel
|
||||
restarts after restore. Destructive.
|
||||
url: '#restore-the-panel-db-from-an-uploaded-backup-multipart-form-field-name-db-sqlite-panels-accept-a-sqlite-database-db-or-a-sqlite-migration-dump-dump-postgresql-panels-accept-a-pg_dump-archive-dump-a-sqlite-database-db-or-a-sqlite-migration-dump-the-panel-restarts-after-restore-destructive'
|
||||
- depth: 2
|
||||
title: Generate a new ECH (Encrypted Client Hello) keypair and config list for
|
||||
the given SNI.
|
||||
url: >-
|
||||
#generate-a-new-ech-encrypted-client-hello-keypair-and-config-list-for-the-given-sni
|
||||
url: '#generate-a-new-ech-encrypted-client-hello-keypair-and-config-list-for-the-given-sni'
|
||||
- depth: 2
|
||||
title: >-
|
||||
Compute the hex SHA-256 of a certificate (DER) for pinning
|
||||
title: Compute the hex SHA-256 of a certificate (DER) for pinning
|
||||
(pinnedPeerCertSha256). Provide either a server file path or inline
|
||||
PEM/DER content.
|
||||
url: >-
|
||||
#compute-the-hex-sha-256-of-a-certificate-der-for-pinning-pinnedpeercertsha256-provide-either-a-server-file-path-or-inline-pemder-content
|
||||
url: '#compute-the-hex-sha-256-of-a-certificate-der-for-pinning-pinnedpeercertsha256-provide-either-a-server-file-path-or-inline-pemder-content'
|
||||
- depth: 2
|
||||
title: >-
|
||||
Run `xray tls ping` against a remote server and return its live
|
||||
title: Run `xray tls ping` against a remote server and return its live
|
||||
leaf-certificate SHA-256 hash(es) for pinning (pinnedPeerCertSha256).
|
||||
url: >-
|
||||
#run-xray-tls-ping-against-a-remote-server-and-return-its-live-leaf-certificate-sha-256-hashes-for-pinning-pinnedpeercertsha256
|
||||
url: '#run-xray-tls-ping-against-a-remote-server-and-return-its-live-leaf-certificate-sha-256-hashes-for-pinning-pinnedpeercertsha256'
|
||||
- depth: 2
|
||||
title: >-
|
||||
Fetch the fully aggregated inbound_client_ips database table. Used by
|
||||
title: Run a live TLS 1.3 probe against a candidate REALITY target and return a
|
||||
feasibility verdict (TLS 1.3 + h2 + X25519 + trusted certificate) plus
|
||||
the certificate SAN DNS names. A target on a private/loopback address is
|
||||
reported with privateTarget=true and probed only when allowPrivate is
|
||||
set.
|
||||
url: '#run-a-live-tls-13-probe-against-a-candidate-reality-target-and-return-a-feasibility-verdict-tls-13--h2--x25519--trusted-certificate-plus-the-certificate-san-dns-names-a-target-on-a-privateloopback-address-is-reported-with-privatetargettrue-and-probed-only-when-allowprivate-is-set'
|
||||
- depth: 2
|
||||
title: Probe/discover REALITY targets and return each verdict ranked by
|
||||
feasibility then latency. Each comma-separated token may be a domain
|
||||
(validated with SNI), a bare IP, or a CIDR range (discovered without SNI
|
||||
by reading the certificate domain). When empty, a built-in seed list is
|
||||
probed.
|
||||
url: '#probediscover-reality-targets-and-return-each-verdict-ranked-by-feasibility-then-latency-each-comma-separated-token-may-be-a-domain-validated-with-sni-a-bare-ip-or-a-cidr-range-discovered-without-sni-by-reading-the-certificate-domain-when-empty-a-built-in-seed-list-is-probed'
|
||||
- depth: 2
|
||||
title: Fetch the fully aggregated inbound_client_ips database table. Used by
|
||||
nodes to sync recently active IPs across the cluster.
|
||||
url: >-
|
||||
#fetch-the-fully-aggregated-inbound_client_ips-database-table-used-by-nodes-to-sync-recently-active-ips-across-the-cluster
|
||||
url: '#fetch-the-fully-aggregated-inbound_client_ips-database-table-used-by-nodes-to-sync-recently-active-ips-across-the-cluster'
|
||||
- depth: 2
|
||||
title: >-
|
||||
Submit a list of recently active IP timestamps. The panel merges them
|
||||
title: Submit a list of recently active IP timestamps. The panel merges them
|
||||
with the existing database to maintain a unified global IP-limit view.
|
||||
url: >-
|
||||
#submit-a-list-of-recently-active-ip-timestamps-the-panel-merges-them-with-the-existing-database-to-maintain-a-unified-global-ip-limit-view
|
||||
url: '#submit-a-list-of-recently-active-ip-timestamps-the-panel-merges-them-with-the-existing-database-to-maintain-a-unified-global-ip-limit-view'
|
||||
structuredData:
|
||||
headings:
|
||||
- content: >-
|
||||
Real-time machine snapshot: CPU, memory, swap, disk, network IO, load
|
||||
- content: Serve this API description as an OpenAPI 3 document — the same file
|
||||
that powers the API Docs page. Requires a session or Bearer token like
|
||||
the rest of /panel/api. Useful for generating clients or importing
|
||||
into API tooling.
|
||||
id: serve-this-api-description-as-an-openapi-3-document--the-same-file-that-powers-the-api-docs-page-requires-a-session-or-bearer-token-like-the-rest-of-panelapi-useful-for-generating-clients-or-importing-into-api-tooling
|
||||
- content: 'Real-time machine snapshot: CPU, memory, swap, disk, network IO, load
|
||||
averages, open connections, Xray state. Cached and refreshed every 2
|
||||
seconds in the background.
|
||||
id: >-
|
||||
real-time-machine-snapshot-cpu-memory-swap-disk-network-io-load-averages-open-connections-xray-state-cached-and-refreshed-every-2-seconds-in-the-background
|
||||
- content: >-
|
||||
Reports whether per-client IP limits can be enforced on this host. The
|
||||
seconds in the background.'
|
||||
id: real-time-machine-snapshot-cpu-memory-swap-disk-network-io-load-averages-open-connections-xray-state-cached-and-refreshed-every-2-seconds-in-the-background
|
||||
- content: Reports whether per-client IP limits can be enforced on this host. The
|
||||
panel uses it to gate the "IP Limit" field, since enforcement depends
|
||||
on Fail2ban being installed.
|
||||
id: >-
|
||||
reports-whether-per-client-ip-limits-can-be-enforced-on-this-host-the-panel-uses-it-to-gate-the-ip-limit-field-since-enforcement-depends-on-fail2ban-being-installed
|
||||
- content: >-
|
||||
Legacy: aggregated CPU history. Use /history/cpu/:bucket instead —
|
||||
same data with a uniform {t, v} shape.
|
||||
id: >-
|
||||
legacy-aggregated-cpu-history-use-historycpubucket-instead--same-data-with-a-uniform-t-v-shape
|
||||
- content: >-
|
||||
Aggregated time-series for one metric. Returns an array of {t, v}
|
||||
id: reports-whether-per-client-ip-limits-can-be-enforced-on-this-host-the-panel-uses-it-to-gate-the-ip-limit-field-since-enforcement-depends-on-fail2ban-being-installed
|
||||
- content: 'Legacy: aggregated CPU history. Use /history/cpu/:bucket instead —
|
||||
same data with a uniform {t, v} shape.'
|
||||
id: legacy-aggregated-cpu-history-use-historycpubucket-instead--same-data-with-a-uniform-t-v-shape
|
||||
- content: Aggregated time-series for one metric. Returns an array of {t, v}
|
||||
samples covering the last ~6 hours.
|
||||
id: >-
|
||||
aggregated-time-series-for-one-metric-returns-an-array-of-t-v-samples-covering-the-last-6-hours
|
||||
- content: >-
|
||||
Xray runtime metrics state — whether the xray config has a `metrics`
|
||||
id: aggregated-time-series-for-one-metric-returns-an-array-of-t-v-samples-covering-the-last-6-hours
|
||||
- content: Xray runtime metrics state — whether the xray config has a `metrics`
|
||||
block, which expvar keys are flowing, and the current snapshot values
|
||||
for each. Returns an empty state when metrics are not configured.
|
||||
id: >-
|
||||
xray-runtime-metrics-state--whether-the-xray-config-has-a-metrics-block-which-expvar-keys-are-flowing-and-the-current-snapshot-values-for-each-returns-an-empty-state-when-metrics-are-not-configured
|
||||
- content: >-
|
||||
Time-series history for one Xray runtime metric over the last ~6
|
||||
hours. Same {t, v} shape as /history/:metric/:bucket.
|
||||
id: >-
|
||||
time-series-history-for-one-xray-runtime-metric-over-the-last-6-hours-same-t-v-shape-as-historymetricbucket
|
||||
- content: >-
|
||||
Latest snapshot from the Xray observatory — per-outbound latency,
|
||||
id: xray-runtime-metrics-state--whether-the-xray-config-has-a-metrics-block-which-expvar-keys-are-flowing-and-the-current-snapshot-values-for-each-returns-an-empty-state-when-metrics-are-not-configured
|
||||
- content: Time-series history for one Xray runtime metric over the last ~6 hours.
|
||||
Same {t, v} shape as /history/:metric/:bucket.
|
||||
id: time-series-history-for-one-xray-runtime-metric-over-the-last-6-hours-same-t-v-shape-as-historymetricbucket
|
||||
- content: Latest snapshot from the Xray observatory — per-outbound latency,
|
||||
health status, and last-probe time. Only populated when the Xray
|
||||
config has an observatory configured.
|
||||
id: >-
|
||||
latest-snapshot-from-the-xray-observatory--per-outbound-latency-health-status-and-last-probe-time-only-populated-when-the-xray-config-has-an-observatory-configured
|
||||
- content: >-
|
||||
Time-series of observatory probe results for one outbound tag. Same
|
||||
{t, v} shape as the other history endpoints.
|
||||
id: >-
|
||||
time-series-of-observatory-probe-results-for-one-outbound-tag-same-t-v-shape-as-the-other-history-endpoints
|
||||
id: latest-snapshot-from-the-xray-observatory--per-outbound-latency-health-status-and-last-probe-time-only-populated-when-the-xray-config-has-an-observatory-configured
|
||||
- content: Time-series of observatory probe results for one outbound tag. Same {t,
|
||||
v} shape as the other history endpoints.
|
||||
id: time-series-of-observatory-probe-results-for-one-outbound-tag-same-t-v-shape-as-the-other-history-endpoints
|
||||
- content: List Xray binary versions available for install on this host.
|
||||
id: list-xray-binary-versions-available-for-install-on-this-host
|
||||
- content: Check whether a newer 3x-ui release is available on GitHub.
|
||||
id: check-whether-a-newer-3x-ui-release-is-available-on-github
|
||||
- content: >-
|
||||
Return the assembled Xray config that’s currently running on this
|
||||
host.
|
||||
- content: Report the outcome of the most recently launched panel self-update (see
|
||||
POST updatePanel). Compare the returned runId against the one
|
||||
updatePanel returned to tell this run apart from a stale result.
|
||||
id: report-the-outcome-of-the-most-recently-launched-panel-self-update-see-post-updatepanel-compare-the-returned-runid-against-the-one-updatepanel-returned-to-tell-this-run-apart-from-a-stale-result
|
||||
- content: Return the assembled Xray config that’s currently running on this host.
|
||||
id: return-the-assembled-xray-config-thats-currently-running-on-this-host
|
||||
- content: >-
|
||||
Stream the SQLite database file as an attachment. Use as a manual
|
||||
backup.
|
||||
id: >-
|
||||
stream-the-sqlite-database-file-as-an-attachment-use-as-a-manual-backup
|
||||
- content: >-
|
||||
Stream a cross-engine migration file as an attachment: a .dump (SQL
|
||||
- content: 'Stream a full database backup as an attachment: the SQLite .db file on
|
||||
SQLite panels, or a pg_dump custom-format archive (.dump) on
|
||||
PostgreSQL panels. Use as a manual backup.'
|
||||
id: stream-a-full-database-backup-as-an-attachment-the-sqlite-db-file-on-sqlite-panels-or-a-pg_dump-custom-format-archive-dump-on-postgresql-panels-use-as-a-manual-backup
|
||||
- content: 'Stream a cross-engine migration file as an attachment: a .dump (SQL
|
||||
text) on SQLite, or a .db SQLite database built from the live data on
|
||||
PostgreSQL.
|
||||
id: >-
|
||||
stream-a-cross-engine-migration-file-as-an-attachment-a-dump-sql-text-on-sqlite-or-a-db-sqlite-database-built-from-the-live-data-on-postgresql
|
||||
PostgreSQL.'
|
||||
id: stream-a-cross-engine-migration-file-as-an-attachment-a-dump-sql-text-on-sqlite-or-a-db-sqlite-database-built-from-the-live-data-on-postgresql
|
||||
- content: Generate a fresh UUID v4. Convenience helper for client IDs.
|
||||
id: generate-a-fresh-uuid-v4-convenience-helper-for-client-ids
|
||||
- content: >-
|
||||
Return this panel's own web TLS certificate and key file paths. The
|
||||
- content: Return this panel's own web TLS certificate and key file paths. The
|
||||
central panel calls it on a node (via the node API token) so "Set Cert
|
||||
from Panel" fills a node-assigned inbound with paths that exist on the
|
||||
node.
|
||||
id: >-
|
||||
return-this-panels-own-web-tls-certificate-and-key-file-paths-the-central-panel-calls-it-on-a-node-via-the-node-api-token-so-set-cert-from-panel-fills-a-node-assigned-inbound-with-paths-that-exist-on-the-node
|
||||
- content: >-
|
||||
Read-only summaries (guid, parentGuid, name, address, status,
|
||||
versions) of the nodes this panel manages. A parent panel calls it on
|
||||
a node (via the node API token) to surface transitive sub-nodes in a
|
||||
chained topology. Counts are computed by the parent, not returned
|
||||
here.
|
||||
id: >-
|
||||
read-only-summaries-guid-parentguid-name-address-status-versions-of-the-nodes-this-panel-manages-a-parent-panel-calls-it-on-a-node-via-the-node-api-token-to-surface-transitive-sub-nodes-in-a-chained-topology-counts-are-computed-by-the-parent-not-returned-here
|
||||
id: return-this-panels-own-web-tls-certificate-and-key-file-paths-the-central-panel-calls-it-on-a-node-via-the-node-api-token-so-set-cert-from-panel-fills-a-node-assigned-inbound-with-paths-that-exist-on-the-node
|
||||
- content: Read-only summaries (guid, parentGuid, name, address, status, versions)
|
||||
of the nodes this panel manages. A parent panel calls it on a node
|
||||
(via the node API token) to surface transitive sub-nodes in a chained
|
||||
topology. Counts are computed by the parent, not returned here.
|
||||
id: read-only-summaries-guid-parentguid-name-address-status-versions-of-the-nodes-this-panel-manages-a-parent-panel-calls-it-on-a-node-via-the-node-api-token-to-surface-transitive-sub-nodes-in-a-chained-topology-counts-are-computed-by-the-parent-not-returned-here
|
||||
- content: Generate a new X25519 keypair for Reality.
|
||||
id: generate-a-new-x25519-keypair-for-reality
|
||||
- content: >-
|
||||
Generate a new ML-DSA-65 keypair (post-quantum signature). Returns
|
||||
- content: Generate a new ML-DSA-65 keypair (post-quantum signature). Returns
|
||||
{privateKey, publicKey, seed}.
|
||||
id: >-
|
||||
generate-a-new-ml-dsa-65-keypair-post-quantum-signature-returns-privatekey-publickey-seed
|
||||
- content: >-
|
||||
Generate a new ML-KEM-768 keypair (post-quantum KEM). Returns
|
||||
id: generate-a-new-ml-dsa-65-keypair-post-quantum-signature-returns-privatekey-publickey-seed
|
||||
- content: Generate a new ML-KEM-768 keypair (post-quantum KEM). Returns
|
||||
{clientKey, serverKey}.
|
||||
id: >-
|
||||
generate-a-new-ml-kem-768-keypair-post-quantum-kem-returns-clientkey-serverkey
|
||||
- content: >-
|
||||
Generate VLESS encryption auth options. Returns an auths array each
|
||||
id: generate-a-new-ml-kem-768-keypair-post-quantum-kem-returns-clientkey-serverkey
|
||||
- content: Generate VLESS encryption auth options. Returns an auths array each
|
||||
with id, label, encryption, and decryption fields.
|
||||
id: >-
|
||||
generate-vless-encryption-auth-options-returns-an-auths-array-each-with-id-label-encryption-and-decryption-fields
|
||||
id: generate-vless-encryption-auth-options-returns-an-auths-array-each-with-id-label-encryption-and-decryption-fields
|
||||
- content: Stop the Xray binary. All proxies go offline immediately.
|
||||
id: stop-the-xray-binary-all-proxies-go-offline-immediately
|
||||
- content: >-
|
||||
Reload Xray with the current config. Typically required after
|
||||
- content: Reload Xray with the current config. Typically required after
|
||||
structural inbound or routing changes.
|
||||
id: >-
|
||||
reload-xray-with-the-current-config-typically-required-after-structural-inbound-or-routing-changes
|
||||
- content: >-
|
||||
Download and install the specified Xray version. Pass "latest" for the
|
||||
id: reload-xray-with-the-current-config-typically-required-after-structural-inbound-or-routing-changes
|
||||
- content: Download and install the specified Xray version. Pass "latest" for the
|
||||
newest release.
|
||||
id: >-
|
||||
download-and-install-the-specified-xray-version-pass-latest-for-the-newest-release
|
||||
- content: >-
|
||||
Self-update the panel to the latest version. The server restarts on
|
||||
id: download-and-install-the-specified-xray-version-pass-latest-for-the-newest-release
|
||||
- content: Self-update the panel to the latest version. The server restarts on
|
||||
success.
|
||||
id: >-
|
||||
self-update-the-panel-to-the-latest-version-the-server-restarts-on-success
|
||||
- content: >-
|
||||
Toggle the panel update channel between stable and the rolling
|
||||
id: self-update-the-panel-to-the-latest-version-the-server-restarts-on-success
|
||||
- content: Toggle the panel update channel between stable and the rolling
|
||||
per-commit dev release. Only effective on dev builds.
|
||||
id: >-
|
||||
toggle-the-panel-update-channel-between-stable-and-the-rolling-per-commit-dev-release-only-effective-on-dev-builds
|
||||
- content: >-
|
||||
Refresh the default GeoIP / GeoSite data files. Body can include a
|
||||
id: toggle-the-panel-update-channel-between-stable-and-the-rolling-per-commit-dev-release-only-effective-on-dev-builds
|
||||
- content: Refresh the default GeoIP / GeoSite data files. Body can include a
|
||||
fileName, or use the /:fileName variant.
|
||||
id: >-
|
||||
refresh-the-default-geoip--geosite-data-files-body-can-include-a-filename-or-use-the-filename-variant
|
||||
id: refresh-the-default-geoip--geosite-data-files-body-can-include-a-filename-or-use-the-filename-variant
|
||||
- content: Refresh a single Geo file by filename (e.g. geoip.dat, geosite.dat).
|
||||
id: refresh-a-single-geo-file-by-filename-eg-geoipdat-geositedat
|
||||
- content: Return the last N lines of the panel’s own log.
|
||||
id: return-the-last-n-lines-of-the-panels-own-log
|
||||
- content: Return the last N lines of the Xray process log.
|
||||
id: return-the-last-n-lines-of-the-xray-process-log
|
||||
- content: >-
|
||||
Restore the panel DB from an uploaded SQLite file (multipart form,
|
||||
field name "db"). The panel restarts after restore. Destructive.
|
||||
id: >-
|
||||
restore-the-panel-db-from-an-uploaded-sqlite-file-multipart-form-field-name-db-the-panel-restarts-after-restore-destructive
|
||||
- content: >-
|
||||
Generate a new ECH (Encrypted Client Hello) keypair and config list
|
||||
for the given SNI.
|
||||
id: >-
|
||||
generate-a-new-ech-encrypted-client-hello-keypair-and-config-list-for-the-given-sni
|
||||
- content: >-
|
||||
Compute the hex SHA-256 of a certificate (DER) for pinning
|
||||
- content: Return live AmneziaWG peer activity (handshake, endpoint, transfer)
|
||||
plus the panel’s own AmneziaWG event lines.
|
||||
id: return-live-amneziawg-peer-activity-handshake-endpoint-transfer-plus-the-panels-own-amneziawg-event-lines
|
||||
- content: Restore the panel DB from an uploaded backup (multipart form, field
|
||||
name "db"). SQLite panels accept a SQLite database (.db) or a SQLite
|
||||
migration dump (.dump); PostgreSQL panels accept a pg_dump archive
|
||||
(.dump), a SQLite database (.db), or a SQLite migration dump. The
|
||||
panel restarts after restore. Destructive.
|
||||
id: restore-the-panel-db-from-an-uploaded-backup-multipart-form-field-name-db-sqlite-panels-accept-a-sqlite-database-db-or-a-sqlite-migration-dump-dump-postgresql-panels-accept-a-pg_dump-archive-dump-a-sqlite-database-db-or-a-sqlite-migration-dump-the-panel-restarts-after-restore-destructive
|
||||
- content: Generate a new ECH (Encrypted Client Hello) keypair and config list for
|
||||
the given SNI.
|
||||
id: generate-a-new-ech-encrypted-client-hello-keypair-and-config-list-for-the-given-sni
|
||||
- content: Compute the hex SHA-256 of a certificate (DER) for pinning
|
||||
(pinnedPeerCertSha256). Provide either a server file path or inline
|
||||
PEM/DER content.
|
||||
id: >-
|
||||
compute-the-hex-sha-256-of-a-certificate-der-for-pinning-pinnedpeercertsha256-provide-either-a-server-file-path-or-inline-pemder-content
|
||||
- content: >-
|
||||
Run `xray tls ping` against a remote server and return its live
|
||||
id: compute-the-hex-sha-256-of-a-certificate-der-for-pinning-pinnedpeercertsha256-provide-either-a-server-file-path-or-inline-pemder-content
|
||||
- content: Run `xray tls ping` against a remote server and return its live
|
||||
leaf-certificate SHA-256 hash(es) for pinning (pinnedPeerCertSha256).
|
||||
id: >-
|
||||
run-xray-tls-ping-against-a-remote-server-and-return-its-live-leaf-certificate-sha-256-hashes-for-pinning-pinnedpeercertsha256
|
||||
- content: >-
|
||||
Fetch the fully aggregated inbound_client_ips database table. Used by
|
||||
id: run-xray-tls-ping-against-a-remote-server-and-return-its-live-leaf-certificate-sha-256-hashes-for-pinning-pinnedpeercertsha256
|
||||
- content: Run a live TLS 1.3 probe against a candidate REALITY target and return
|
||||
a feasibility verdict (TLS 1.3 + h2 + X25519 + trusted certificate)
|
||||
plus the certificate SAN DNS names. A target on a private/loopback
|
||||
address is reported with privateTarget=true and probed only when
|
||||
allowPrivate is set.
|
||||
id: run-a-live-tls-13-probe-against-a-candidate-reality-target-and-return-a-feasibility-verdict-tls-13--h2--x25519--trusted-certificate-plus-the-certificate-san-dns-names-a-target-on-a-privateloopback-address-is-reported-with-privatetargettrue-and-probed-only-when-allowprivate-is-set
|
||||
- content: Probe/discover REALITY targets and return each verdict ranked by
|
||||
feasibility then latency. Each comma-separated token may be a domain
|
||||
(validated with SNI), a bare IP, or a CIDR range (discovered without
|
||||
SNI by reading the certificate domain). When empty, a built-in seed
|
||||
list is probed.
|
||||
id: probediscover-reality-targets-and-return-each-verdict-ranked-by-feasibility-then-latency-each-comma-separated-token-may-be-a-domain-validated-with-sni-a-bare-ip-or-a-cidr-range-discovered-without-sni-by-reading-the-certificate-domain-when-empty-a-built-in-seed-list-is-probed
|
||||
- content: Fetch the fully aggregated inbound_client_ips database table. Used by
|
||||
nodes to sync recently active IPs across the cluster.
|
||||
id: >-
|
||||
fetch-the-fully-aggregated-inbound_client_ips-database-table-used-by-nodes-to-sync-recently-active-ips-across-the-cluster
|
||||
- content: >-
|
||||
Submit a list of recently active IP timestamps. The panel merges them
|
||||
id: fetch-the-fully-aggregated-inbound_client_ips-database-table-used-by-nodes-to-sync-recently-active-ips-across-the-cluster
|
||||
- content: Submit a list of recently active IP timestamps. The panel merges them
|
||||
with the existing database to maintain a unified global IP-limit view.
|
||||
id: >-
|
||||
submit-a-list-of-recently-active-ip-timestamps-the-panel-merges-them-with-the-existing-database-to-maintain-a-unified-global-ip-limit-view
|
||||
id: submit-a-list-of-recently-active-ip-timestamps-the-panel-merges-them-with-the-existing-database-to-maintain-a-unified-global-ip-limit-view
|
||||
contents: []
|
||||
---
|
||||
|
||||
@@ -375,7 +329,7 @@ export default function Layout(props) {
|
||||
return (
|
||||
<>
|
||||
{props.children}
|
||||
<Comp document="./public/openapi.json" webhooks={[]} operations={[{"path":"/panel/api/server/status","method":"get"},{"path":"/panel/api/server/fail2banStatus","method":"get"},{"path":"/panel/api/server/cpuHistory/{bucket}","method":"get"},{"path":"/panel/api/server/history/{metric}/{bucket}","method":"get"},{"path":"/panel/api/server/xrayMetricsState","method":"get"},{"path":"/panel/api/server/xrayMetricsHistory/{metric}/{bucket}","method":"get"},{"path":"/panel/api/server/xrayObservatory","method":"get"},{"path":"/panel/api/server/xrayObservatoryHistory/{tag}/{bucket}","method":"get"},{"path":"/panel/api/server/getXrayVersion","method":"get"},{"path":"/panel/api/server/getPanelUpdateInfo","method":"get"},{"path":"/panel/api/server/getConfigJson","method":"get"},{"path":"/panel/api/server/getDb","method":"get"},{"path":"/panel/api/server/getMigration","method":"get"},{"path":"/panel/api/server/getNewUUID","method":"get"},{"path":"/panel/api/server/getWebCertFiles","method":"get"},{"path":"/panel/api/server/descendants","method":"get"},{"path":"/panel/api/server/getNewX25519Cert","method":"get"},{"path":"/panel/api/server/getNewmldsa65","method":"get"},{"path":"/panel/api/server/getNewmlkem768","method":"get"},{"path":"/panel/api/server/getNewVlessEnc","method":"get"},{"path":"/panel/api/server/stopXrayService","method":"post"},{"path":"/panel/api/server/restartXrayService","method":"post"},{"path":"/panel/api/server/installXray/{version}","method":"post"},{"path":"/panel/api/server/updatePanel","method":"post"},{"path":"/panel/api/server/setUpdateChannel","method":"post"},{"path":"/panel/api/server/updateGeofile","method":"post"},{"path":"/panel/api/server/updateGeofile/{fileName}","method":"post"},{"path":"/panel/api/server/logs/{count}","method":"post"},{"path":"/panel/api/server/xraylogs/{count}","method":"post"},{"path":"/panel/api/server/importDB","method":"post"},{"path":"/panel/api/server/getNewEchCert","method":"post"},{"path":"/panel/api/server/getCertHash","method":"post"},{"path":"/panel/api/server/getRemoteCertHash","method":"post"},{"path":"/panel/api/server/clientIps","method":"get"},{"path":"/panel/api/server/clientIps","method":"post"}]} showTitle />
|
||||
<Comp document="./public/openapi.json" webhooks={[]} operations={[{"path":"/panel/api/openapi.json","method":"get"},{"path":"/panel/api/server/status","method":"get"},{"path":"/panel/api/server/fail2banStatus","method":"get"},{"path":"/panel/api/server/cpuHistory/{bucket}","method":"get"},{"path":"/panel/api/server/history/{metric}/{bucket}","method":"get"},{"path":"/panel/api/server/xrayMetricsState","method":"get"},{"path":"/panel/api/server/xrayMetricsHistory/{metric}/{bucket}","method":"get"},{"path":"/panel/api/server/xrayObservatory","method":"get"},{"path":"/panel/api/server/xrayObservatoryHistory/{tag}/{bucket}","method":"get"},{"path":"/panel/api/server/getXrayVersion","method":"get"},{"path":"/panel/api/server/getPanelUpdateInfo","method":"get"},{"path":"/panel/api/server/getUpdateStatus","method":"get"},{"path":"/panel/api/server/getConfigJson","method":"get"},{"path":"/panel/api/server/getDb","method":"get"},{"path":"/panel/api/server/getMigration","method":"get"},{"path":"/panel/api/server/getNewUUID","method":"get"},{"path":"/panel/api/server/getWebCertFiles","method":"get"},{"path":"/panel/api/server/descendants","method":"get"},{"path":"/panel/api/server/getNewX25519Cert","method":"get"},{"path":"/panel/api/server/getNewmldsa65","method":"get"},{"path":"/panel/api/server/getNewmlkem768","method":"get"},{"path":"/panel/api/server/getNewVlessEnc","method":"get"},{"path":"/panel/api/server/stopXrayService","method":"post"},{"path":"/panel/api/server/restartXrayService","method":"post"},{"path":"/panel/api/server/installXray/{version}","method":"post"},{"path":"/panel/api/server/updatePanel","method":"post"},{"path":"/panel/api/server/setUpdateChannel","method":"post"},{"path":"/panel/api/server/updateGeofile","method":"post"},{"path":"/panel/api/server/updateGeofile/{fileName}","method":"post"},{"path":"/panel/api/server/logs/{count}","method":"post"},{"path":"/panel/api/server/xraylogs/{count}","method":"post"},{"path":"/panel/api/server/amneziawglogs/{count}","method":"post"},{"path":"/panel/api/server/importDB","method":"post"},{"path":"/panel/api/server/getNewEchCert","method":"post"},{"path":"/panel/api/server/getCertHash","method":"post"},{"path":"/panel/api/server/getRemoteCertHash","method":"post"},{"path":"/panel/api/server/scanRealityTarget","method":"post"},{"path":"/panel/api/server/scanRealityTargets","method":"post"},{"path":"/panel/api/server/clientIps","method":"get"},{"path":"/panel/api/server/clientIps","method":"post"}]} showTitle />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
---
|
||||
title: Settings
|
||||
description: >-
|
||||
Panel configuration and user credentials. All endpoints live under
|
||||
description: Panel configuration and user credentials. All endpoints live under
|
||||
/panel/api/setting and require a logged-in session or Bearer token.
|
||||
full: true
|
||||
_openapi:
|
||||
@@ -9,101 +8,87 @@ _openapi:
|
||||
- ./public/openapi.json
|
||||
toc:
|
||||
- depth: 2
|
||||
title: >-
|
||||
Return every panel setting: web server, Telegram bot, subscription,
|
||||
security, LDAP. The full JSON blob that the Settings page edits.
|
||||
url: >-
|
||||
#return-every-panel-setting-web-server-telegram-bot-subscription-security-ldap-the-full-json-blob-that-the-settings-page-edits
|
||||
title: 'Return every panel setting: web server, Telegram bot, subscription,
|
||||
security, LDAP. The full JSON blob that the Settings page edits.'
|
||||
url: '#return-every-panel-setting-web-server-telegram-bot-subscription-security-ldap-the-full-json-blob-that-the-settings-page-edits'
|
||||
- depth: 2
|
||||
title: >-
|
||||
Return the computed default settings based on the request host. Useful
|
||||
to preview what a fresh install would use.
|
||||
url: >-
|
||||
#return-the-computed-default-settings-based-on-the-request-host-useful-to-preview-what-a-fresh-install-would-use
|
||||
title: Return the computed default settings based on the request host. Useful to
|
||||
preview what a fresh install would use.
|
||||
url: '#return-the-computed-default-settings-based-on-the-request-host-useful-to-preview-what-a-fresh-install-would-use'
|
||||
- depth: 2
|
||||
title: >-
|
||||
Persist every setting at once. The body mirrors the shape returned by
|
||||
title: Return the shipped (factory) default value per browser-safe setting key,
|
||||
so clients can tell a stored value apart from the default it would fall
|
||||
back to. Per-install material (secret, panelGuid, mTLS keys) and
|
||||
credential fields are never included.
|
||||
url: '#return-the-shipped-factory-default-value-per-browser-safe-setting-key-so-clients-can-tell-a-stored-value-apart-from-the-default-it-would-fall-back-to-per-install-material-secret-panelguid-mtls-keys-and-credential-fields-are-never-included'
|
||||
- depth: 2
|
||||
title: Persist every setting at once. The body mirrors the shape returned by
|
||||
/all. Invalid values (bad ports, missing cert pairs, etc.) are rejected
|
||||
before write.
|
||||
url: >-
|
||||
#persist-every-setting-at-once-the-body-mirrors-the-shape-returned-by-all-invalid-values-bad-ports-missing-cert-pairs-etc-are-rejected-before-write
|
||||
url: '#persist-every-setting-at-once-the-body-mirrors-the-shape-returned-by-all-invalid-values-bad-ports-missing-cert-pairs-etc-are-rejected-before-write'
|
||||
- depth: 2
|
||||
title: >-
|
||||
Change the panel admin username and password. Requires the current
|
||||
title: Validate any regular expression with the backend Go RE2 compiler without
|
||||
saving it.
|
||||
url: '#validate-any-regular-expression-with-the-backend-go-re2-compiler-without-saving-it'
|
||||
- depth: 2
|
||||
title: Change the panel admin username and password. Requires the current
|
||||
credentials for verification. The session is refreshed with the new
|
||||
values on success.
|
||||
url: >-
|
||||
#change-the-panel-admin-username-and-password-requires-the-current-credentials-for-verification-the-session-is-refreshed-with-the-new-values-on-success
|
||||
url: '#change-the-panel-admin-username-and-password-requires-the-current-credentials-for-verification-the-session-is-refreshed-with-the-new-values-on-success'
|
||||
- depth: 2
|
||||
title: >-
|
||||
Restart the entire 3x-ui process after a 3-second grace period. The
|
||||
title: Restart the entire 3x-ui process after a 3-second grace period. The
|
||||
connection drops immediately; the panel comes back online ~5-10 seconds
|
||||
later.
|
||||
url: >-
|
||||
#restart-the-entire-3x-ui-process-after-a-3-second-grace-period-the-connection-drops-immediately-the-panel-comes-back-online-5-10-seconds-later
|
||||
url: '#restart-the-entire-3x-ui-process-after-a-3-second-grace-period-the-connection-drops-immediately-the-panel-comes-back-online-5-10-seconds-later'
|
||||
- depth: 2
|
||||
title: >-
|
||||
Test SMTP connection with stage-by-stage reporting (connect, auth,
|
||||
send). Returns structured result with stage and message.
|
||||
url: >-
|
||||
#test-smtp-connection-with-stage-by-stage-reporting-connect-auth-send-returns-structured-result-with-stage-and-message
|
||||
title: Test SMTP connection with stage-by-stage reporting (connect, auth, send).
|
||||
Returns structured result with stage and message.
|
||||
url: '#test-smtp-connection-with-stage-by-stage-reporting-connect-auth-send-returns-structured-result-with-stage-and-message'
|
||||
- depth: 2
|
||||
title: >-
|
||||
Test Telegram bot connection by sending a test message to the configured
|
||||
title: Test Telegram bot connection by sending a test message to the configured
|
||||
chat.
|
||||
url: >-
|
||||
#test-telegram-bot-connection-by-sending-a-test-message-to-the-configured-chat
|
||||
url: '#test-telegram-bot-connection-by-sending-a-test-message-to-the-configured-chat'
|
||||
- depth: 2
|
||||
title: >-
|
||||
Return the built-in default Xray JSON config template that ships with
|
||||
title: Return the built-in default Xray JSON config template that ships with
|
||||
this panel version.
|
||||
url: >-
|
||||
#return-the-built-in-default-xray-json-config-template-that-ships-with-this-panel-version
|
||||
url: '#return-the-built-in-default-xray-json-config-template-that-ships-with-this-panel-version'
|
||||
structuredData:
|
||||
headings:
|
||||
- content: >-
|
||||
Return every panel setting: web server, Telegram bot, subscription,
|
||||
security, LDAP. The full JSON blob that the Settings page edits.
|
||||
id: >-
|
||||
return-every-panel-setting-web-server-telegram-bot-subscription-security-ldap-the-full-json-blob-that-the-settings-page-edits
|
||||
- content: >-
|
||||
Return the computed default settings based on the request host. Useful
|
||||
- content: 'Return every panel setting: web server, Telegram bot, subscription,
|
||||
security, LDAP. The full JSON blob that the Settings page edits.'
|
||||
id: return-every-panel-setting-web-server-telegram-bot-subscription-security-ldap-the-full-json-blob-that-the-settings-page-edits
|
||||
- content: Return the computed default settings based on the request host. Useful
|
||||
to preview what a fresh install would use.
|
||||
id: >-
|
||||
return-the-computed-default-settings-based-on-the-request-host-useful-to-preview-what-a-fresh-install-would-use
|
||||
- content: >-
|
||||
Persist every setting at once. The body mirrors the shape returned by
|
||||
id: return-the-computed-default-settings-based-on-the-request-host-useful-to-preview-what-a-fresh-install-would-use
|
||||
- content: Return the shipped (factory) default value per browser-safe setting
|
||||
key, so clients can tell a stored value apart from the default it
|
||||
would fall back to. Per-install material (secret, panelGuid, mTLS
|
||||
keys) and credential fields are never included.
|
||||
id: return-the-shipped-factory-default-value-per-browser-safe-setting-key-so-clients-can-tell-a-stored-value-apart-from-the-default-it-would-fall-back-to-per-install-material-secret-panelguid-mtls-keys-and-credential-fields-are-never-included
|
||||
- content: Persist every setting at once. The body mirrors the shape returned by
|
||||
/all. Invalid values (bad ports, missing cert pairs, etc.) are
|
||||
rejected before write.
|
||||
id: >-
|
||||
persist-every-setting-at-once-the-body-mirrors-the-shape-returned-by-all-invalid-values-bad-ports-missing-cert-pairs-etc-are-rejected-before-write
|
||||
- content: >-
|
||||
Change the panel admin username and password. Requires the current
|
||||
id: persist-every-setting-at-once-the-body-mirrors-the-shape-returned-by-all-invalid-values-bad-ports-missing-cert-pairs-etc-are-rejected-before-write
|
||||
- content: Validate any regular expression with the backend Go RE2 compiler
|
||||
without saving it.
|
||||
id: validate-any-regular-expression-with-the-backend-go-re2-compiler-without-saving-it
|
||||
- content: Change the panel admin username and password. Requires the current
|
||||
credentials for verification. The session is refreshed with the new
|
||||
values on success.
|
||||
id: >-
|
||||
change-the-panel-admin-username-and-password-requires-the-current-credentials-for-verification-the-session-is-refreshed-with-the-new-values-on-success
|
||||
- content: >-
|
||||
Restart the entire 3x-ui process after a 3-second grace period. The
|
||||
id: change-the-panel-admin-username-and-password-requires-the-current-credentials-for-verification-the-session-is-refreshed-with-the-new-values-on-success
|
||||
- content: Restart the entire 3x-ui process after a 3-second grace period. The
|
||||
connection drops immediately; the panel comes back online ~5-10
|
||||
seconds later.
|
||||
id: >-
|
||||
restart-the-entire-3x-ui-process-after-a-3-second-grace-period-the-connection-drops-immediately-the-panel-comes-back-online-5-10-seconds-later
|
||||
- content: >-
|
||||
Test SMTP connection with stage-by-stage reporting (connect, auth,
|
||||
id: restart-the-entire-3x-ui-process-after-a-3-second-grace-period-the-connection-drops-immediately-the-panel-comes-back-online-5-10-seconds-later
|
||||
- content: Test SMTP connection with stage-by-stage reporting (connect, auth,
|
||||
send). Returns structured result with stage and message.
|
||||
id: >-
|
||||
test-smtp-connection-with-stage-by-stage-reporting-connect-auth-send-returns-structured-result-with-stage-and-message
|
||||
- content: >-
|
||||
Test Telegram bot connection by sending a test message to the
|
||||
id: test-smtp-connection-with-stage-by-stage-reporting-connect-auth-send-returns-structured-result-with-stage-and-message
|
||||
- content: Test Telegram bot connection by sending a test message to the
|
||||
configured chat.
|
||||
id: >-
|
||||
test-telegram-bot-connection-by-sending-a-test-message-to-the-configured-chat
|
||||
- content: >-
|
||||
Return the built-in default Xray JSON config template that ships with
|
||||
id: test-telegram-bot-connection-by-sending-a-test-message-to-the-configured-chat
|
||||
- content: Return the built-in default Xray JSON config template that ships with
|
||||
this panel version.
|
||||
id: >-
|
||||
return-the-built-in-default-xray-json-config-template-that-ships-with-this-panel-version
|
||||
id: return-the-built-in-default-xray-json-config-template-that-ships-with-this-panel-version
|
||||
contents: []
|
||||
---
|
||||
|
||||
@@ -116,7 +101,7 @@ export default function Layout(props) {
|
||||
return (
|
||||
<>
|
||||
{props.children}
|
||||
<Comp document="./public/openapi.json" webhooks={[]} operations={[{"path":"/panel/api/setting/all","method":"post"},{"path":"/panel/api/setting/defaultSettings","method":"post"},{"path":"/panel/api/setting/update","method":"post"},{"path":"/panel/api/setting/updateUser","method":"post"},{"path":"/panel/api/setting/restartPanel","method":"post"},{"path":"/panel/api/setting/testSmtp","method":"post"},{"path":"/panel/api/setting/testTgBot","method":"post"},{"path":"/panel/api/setting/getDefaultJsonConfig","method":"get"}]} showTitle />
|
||||
<Comp document="./public/openapi.json" webhooks={[]} operations={[{"path":"/panel/api/setting/all","method":"post"},{"path":"/panel/api/setting/defaultSettings","method":"post"},{"path":"/panel/api/setting/factoryDefaults","method":"post"},{"path":"/panel/api/setting/update","method":"post"},{"path":"/panel/api/setting/validateRegex","method":"post"},{"path":"/panel/api/setting/updateUser","method":"post"},{"path":"/panel/api/setting/restartPanel","method":"post"},{"path":"/panel/api/setting/testSmtp","method":"post"},{"path":"/panel/api/setting/testTgBot","method":"post"},{"path":"/panel/api/setting/getDefaultJsonConfig","method":"get"}]} showTitle />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
---
|
||||
title: Subscription Balancers
|
||||
description: 'Client-side balancers for the JSON subscription: each enabled
|
||||
balancer is emitted as one extra config document whose members are the proxy
|
||||
outbounds of the selected inbounds (routing.balancers + burstObservatory).
|
||||
Managed in Settings → Sub Balancers.'
|
||||
full: true
|
||||
_openapi:
|
||||
preload:
|
||||
- ./public/openapi.json
|
||||
toc:
|
||||
- depth: 2
|
||||
title: List all subscription balancers in sort order (sort_order asc, id asc).
|
||||
url: '#list-all-subscription-balancers-in-sort-order-sort_order-asc-id-asc'
|
||||
- depth: 2
|
||||
title: Create a subscription balancer. It appears in the JSON subscription of
|
||||
every client that sits on at least one selected inbound.
|
||||
url: '#create-a-subscription-balancer-it-appears-in-the-json-subscription-of-every-client-that-sits-on-at-least-one-selected-inbound'
|
||||
- depth: 2
|
||||
title: Update a balancer by id. Accepts the same form fields as create (full-row
|
||||
update, including the enabled toggle).
|
||||
url: '#update-a-balancer-by-id-accepts-the-same-form-fields-as-create-full-row-update-including-the-enabled-toggle'
|
||||
- depth: 2
|
||||
title: Delete a balancer by id.
|
||||
url: '#delete-a-balancer-by-id'
|
||||
- depth: 2
|
||||
title: Delete a balancer by id (POST alias of DELETE for clients that cannot
|
||||
send DELETE).
|
||||
url: '#delete-a-balancer-by-id-post-alias-of-delete-for-clients-that-cannot-send-delete'
|
||||
structuredData:
|
||||
headings:
|
||||
- content: List all subscription balancers in sort order (sort_order asc, id asc).
|
||||
id: list-all-subscription-balancers-in-sort-order-sort_order-asc-id-asc
|
||||
- content: Create a subscription balancer. It appears in the JSON subscription of
|
||||
every client that sits on at least one selected inbound.
|
||||
id: create-a-subscription-balancer-it-appears-in-the-json-subscription-of-every-client-that-sits-on-at-least-one-selected-inbound
|
||||
- content: Update a balancer by id. Accepts the same form fields as create
|
||||
(full-row update, including the enabled toggle).
|
||||
id: update-a-balancer-by-id-accepts-the-same-form-fields-as-create-full-row-update-including-the-enabled-toggle
|
||||
- content: Delete a balancer by id.
|
||||
id: delete-a-balancer-by-id
|
||||
- content: Delete a balancer by id (POST alias of DELETE for clients that cannot
|
||||
send DELETE).
|
||||
id: delete-a-balancer-by-id-post-alias-of-delete-for-clients-that-cannot-send-delete
|
||||
contents: []
|
||||
---
|
||||
|
||||
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
|
||||
|
||||
export default function Layout(props) {
|
||||
const { APIPage, OpenAPIPage } = props.components ?? {};
|
||||
// "APIPage" is the old name from v10, this allows both for backward compatibility
|
||||
const Comp = OpenAPIPage ?? APIPage;
|
||||
return (
|
||||
<>
|
||||
{props.children}
|
||||
<Comp document="./public/openapi.json" webhooks={[]} operations={[{"path":"/panel/api/sub-balancers","method":"get"},{"path":"/panel/api/sub-balancers","method":"post"},{"path":"/panel/api/sub-balancers/{id}","method":"post"},{"path":"/panel/api/sub-balancers/{id}","method":"delete"},{"path":"/panel/api/sub-balancers/{id}/del","method":"post"}]} showTitle />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,59 +1,48 @@
|
||||
---
|
||||
title: Subscription Server
|
||||
description: >-
|
||||
A separate HTTP/HTTPS server that serves proxy subscription links (standard,
|
||||
JSON, and Clash) to clients. The server listens on its own port (default
|
||||
10882) and is configured in Settings → Subscription. Paths are configurable;
|
||||
defaults are shown below. All subscription endpoints set response headers for
|
||||
client apps to read traffic/expiry info.
|
||||
description: A separate HTTP/HTTPS server that serves proxy subscription links
|
||||
(standard, JSON, and Clash) to clients. The server listens on its own port
|
||||
(default 10882) and is configured in Settings → Subscription. Paths are
|
||||
configurable; defaults are shown below. All subscription endpoints set
|
||||
response headers for client apps to read traffic/expiry info.
|
||||
full: true
|
||||
_openapi:
|
||||
preload:
|
||||
- ./public/openapi.json
|
||||
toc:
|
||||
- depth: 2
|
||||
title: >-
|
||||
Return base64-encoded subscription links for all enabled clients
|
||||
title: 'Return base64-encoded subscription links for all enabled clients
|
||||
matching the subscription ID. When the request has an Accept: text/html
|
||||
header or ?html=1, renders a styled info page instead. Default path:
|
||||
/sub/:subid.
|
||||
url: >-
|
||||
#return-base64-encoded-subscription-links-for-all-enabled-clients-matching-the-subscription-id-when-the-request-has-an-accept-texthtml-header-or-html1-renders-a-styled-info-page-instead-default-path-subsubid
|
||||
header or ?html=1, renders a styled info page instead. With
|
||||
?format=info, returns the page view-model as JSON (traffic, expiry,
|
||||
online status; no links) for live polling. Default path: /sub/:subid.'
|
||||
url: '#return-base64-encoded-subscription-links-for-all-enabled-clients-matching-the-subscription-id-when-the-request-has-an-accept-texthtml-header-or-html1-renders-a-styled-info-page-instead-with-formatinfo-returns-the-page-view-model-as-json-traffic-expiry-online-status-no-links-for-live-polling-default-path-subsubid'
|
||||
- depth: 2
|
||||
title: >-
|
||||
Return subscription as a JSON array of proxy configs (one per enabled
|
||||
title: 'Return subscription as a JSON array of proxy configs (one per enabled
|
||||
client). Only when JSON subscription is enabled in settings. Default
|
||||
path: /json/:subid.
|
||||
url: >-
|
||||
#return-subscription-as-a-json-array-of-proxy-configs-one-per-enabled-client-only-when-json-subscription-is-enabled-in-settings-default-path-jsonsubid
|
||||
path: /json/:subid.'
|
||||
url: '#return-subscription-as-a-json-array-of-proxy-configs-one-per-enabled-client-only-when-json-subscription-is-enabled-in-settings-default-path-jsonsubid'
|
||||
- depth: 2
|
||||
title: >-
|
||||
Return subscription as a Clash/Mihomo-compatible YAML config, including
|
||||
title: 'Return subscription as a Clash/Mihomo-compatible YAML config, including
|
||||
configured global Clash routing rules. Only when Clash subscription is
|
||||
enabled in settings. Default path: /clash/:subid.
|
||||
url: >-
|
||||
#return-subscription-as-a-clashmihomo-compatible-yaml-config-including-configured-global-clash-routing-rules-only-when-clash-subscription-is-enabled-in-settings-default-path-clashsubid
|
||||
enabled in settings. Default path: /clash/:subid.'
|
||||
url: '#return-subscription-as-a-clashmihomo-compatible-yaml-config-including-configured-global-clash-routing-rules-only-when-clash-subscription-is-enabled-in-settings-default-path-clashsubid'
|
||||
structuredData:
|
||||
headings:
|
||||
- content: >-
|
||||
Return base64-encoded subscription links for all enabled clients
|
||||
- content: 'Return base64-encoded subscription links for all enabled clients
|
||||
matching the subscription ID. When the request has an Accept:
|
||||
text/html header or ?html=1, renders a styled info page instead.
|
||||
Default path: /sub/:subid.
|
||||
id: >-
|
||||
return-base64-encoded-subscription-links-for-all-enabled-clients-matching-the-subscription-id-when-the-request-has-an-accept-texthtml-header-or-html1-renders-a-styled-info-page-instead-default-path-subsubid
|
||||
- content: >-
|
||||
Return subscription as a JSON array of proxy configs (one per enabled
|
||||
text/html header or ?html=1, renders a styled info page instead. With
|
||||
?format=info, returns the page view-model as JSON (traffic, expiry,
|
||||
online status; no links) for live polling. Default path: /sub/:subid.'
|
||||
id: return-base64-encoded-subscription-links-for-all-enabled-clients-matching-the-subscription-id-when-the-request-has-an-accept-texthtml-header-or-html1-renders-a-styled-info-page-instead-with-formatinfo-returns-the-page-view-model-as-json-traffic-expiry-online-status-no-links-for-live-polling-default-path-subsubid
|
||||
- content: 'Return subscription as a JSON array of proxy configs (one per enabled
|
||||
client). Only when JSON subscription is enabled in settings. Default
|
||||
path: /json/:subid.
|
||||
id: >-
|
||||
return-subscription-as-a-json-array-of-proxy-configs-one-per-enabled-client-only-when-json-subscription-is-enabled-in-settings-default-path-jsonsubid
|
||||
- content: >-
|
||||
Return subscription as a Clash/Mihomo-compatible YAML config,
|
||||
path: /json/:subid.'
|
||||
id: return-subscription-as-a-json-array-of-proxy-configs-one-per-enabled-client-only-when-json-subscription-is-enabled-in-settings-default-path-jsonsubid
|
||||
- content: 'Return subscription as a Clash/Mihomo-compatible YAML config,
|
||||
including configured global Clash routing rules. Only when Clash
|
||||
subscription is enabled in settings. Default path: /clash/:subid.
|
||||
id: >-
|
||||
return-subscription-as-a-clashmihomo-compatible-yaml-config-including-configured-global-clash-routing-rules-only-when-clash-subscription-is-enabled-in-settings-default-path-clashsubid
|
||||
subscription is enabled in settings. Default path: /clash/:subid.'
|
||||
id: return-subscription-as-a-clashmihomo-compatible-yaml-config-including-configured-global-clash-routing-rules-only-when-clash-subscription-is-enabled-in-settings-default-path-clashsubid
|
||||
contents: []
|
||||
---
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
---
|
||||
title: WebSocket
|
||||
description: >-
|
||||
Real-time status updates via WebSocket. Connect once at
|
||||
description: Real-time status updates via WebSocket. Connect once at
|
||||
<code>ws://<panel>/ws</code> to receive a stream of JSON messages without
|
||||
polling. Requires an authenticated session cookie (Bearer token auth is not
|
||||
supported). Each message has a <code>type</code> field that identifies the
|
||||
@@ -12,22 +11,18 @@ _openapi:
|
||||
- ./public/openapi.json
|
||||
toc:
|
||||
- depth: 2
|
||||
title: >-
|
||||
Upgrade an HTTP connection to a WebSocket. Requires an authenticated
|
||||
title: Upgrade an HTTP connection to a WebSocket. Requires an authenticated
|
||||
session cookie (Bearer token auth is not supported here). Returns 101
|
||||
Switching Protocols on success. The server then pushes JSON messages
|
||||
described below.
|
||||
url: >-
|
||||
#upgrade-an-http-connection-to-a-websocket-requires-an-authenticated-session-cookie-bearer-token-auth-is-not-supported-here-returns-101-switching-protocols-on-success-the-server-then-pushes-json-messages-described-below
|
||||
url: '#upgrade-an-http-connection-to-a-websocket-requires-an-authenticated-session-cookie-bearer-token-auth-is-not-supported-here-returns-101-switching-protocols-on-success-the-server-then-pushes-json-messages-described-below'
|
||||
structuredData:
|
||||
headings:
|
||||
- content: >-
|
||||
Upgrade an HTTP connection to a WebSocket. Requires an authenticated
|
||||
- content: Upgrade an HTTP connection to a WebSocket. Requires an authenticated
|
||||
session cookie (Bearer token auth is not supported here). Returns 101
|
||||
Switching Protocols on success. The server then pushes JSON messages
|
||||
described below.
|
||||
id: >-
|
||||
upgrade-an-http-connection-to-a-websocket-requires-an-authenticated-session-cookie-bearer-token-auth-is-not-supported-here-returns-101-switching-protocols-on-success-the-server-then-pushes-json-messages-described-below
|
||||
id: upgrade-an-http-connection-to-a-websocket-requires-an-authenticated-session-cookie-bearer-token-auth-is-not-supported-here-returns-101-switching-protocols-on-success-the-server-then-pushes-json-messages-described-below
|
||||
contents: []
|
||||
---
|
||||
|
||||
|
||||
@@ -1,244 +1,210 @@
|
||||
---
|
||||
title: Xray Settings
|
||||
description: >-
|
||||
Xray configuration template, outbound management, Warp/Nord integration, and
|
||||
config testing. All endpoints under /panel/api/xray.
|
||||
description: Xray configuration template, outbound management, Warp/Nord/PIA
|
||||
integration, and config testing. All endpoints under /panel/api/xray.
|
||||
full: true
|
||||
_openapi:
|
||||
preload:
|
||||
- ./public/openapi.json
|
||||
toc:
|
||||
- depth: 2
|
||||
title: >-
|
||||
Return the Xray config template (JSON string), available inbound tags,
|
||||
title: Return the Xray config template (JSON string), available inbound tags,
|
||||
client reverse tags, and the configured outbound test URL in one
|
||||
response.
|
||||
url: >-
|
||||
#return-the-xray-config-template-json-string-available-inbound-tags-client-reverse-tags-and-the-configured-outbound-test-url-in-one-response
|
||||
url: '#return-the-xray-config-template-json-string-available-inbound-tags-client-reverse-tags-and-the-configured-outbound-test-url-in-one-response'
|
||||
- depth: 2
|
||||
title: >-
|
||||
Return the built-in default Xray config shipped with the panel
|
||||
(identical to /panel/api/setting/getDefaultJsonConfig).
|
||||
url: >-
|
||||
#return-the-built-in-default-xray-config-shipped-with-the-panel-identical-to-panelapisettinggetdefaultjsonconfig
|
||||
title: Return the built-in default Xray config shipped with the panel (identical
|
||||
to /panel/api/setting/getDefaultJsonConfig).
|
||||
url: '#return-the-built-in-default-xray-config-shipped-with-the-panel-identical-to-panelapisettinggetdefaultjsonconfig'
|
||||
- depth: 2
|
||||
title: >-
|
||||
Return traffic statistics for every outbound. Each outbound shows
|
||||
title: Return traffic statistics for every outbound. Each outbound shows
|
||||
up/down/total counters.
|
||||
url: >-
|
||||
#return-traffic-statistics-for-every-outbound-each-outbound-shows-updowntotal-counters
|
||||
url: '#return-traffic-statistics-for-every-outbound-each-outbound-shows-updowntotal-counters'
|
||||
- depth: 2
|
||||
title: >-
|
||||
Return the most recent Xray process stdout/stderr output. Useful to
|
||||
check for startup errors or runtime warnings.
|
||||
url: >-
|
||||
#return-the-most-recent-xray-process-stdoutstderr-output-useful-to-check-for-startup-errors-or-runtime-warnings
|
||||
title: Return the most recent Xray process stdout/stderr output. Useful to check
|
||||
for startup errors or runtime warnings.
|
||||
url: '#return-the-most-recent-xray-process-stdoutstderr-output-useful-to-check-for-startup-errors-or-runtime-warnings'
|
||||
- depth: 2
|
||||
title: >-
|
||||
Save the Xray JSON config template and optionally the outbound test URL.
|
||||
title: Save the Xray JSON config template and optionally the outbound test URL.
|
||||
Both are sent as form fields.
|
||||
url: >-
|
||||
#save-the-xray-json-config-template-and-optionally-the-outbound-test-url-both-are-sent-as-form-fields
|
||||
url: '#save-the-xray-json-config-template-and-optionally-the-outbound-test-url-both-are-sent-as-form-fields'
|
||||
- depth: 2
|
||||
title: >-
|
||||
Manage Cloudflare Warp integration. The action parameter selects the
|
||||
title: Manage Cloudflare Warp integration. The action parameter selects the
|
||||
operation.
|
||||
url: >-
|
||||
#manage-cloudflare-warp-integration-the-action-parameter-selects-the-operation
|
||||
url: '#manage-cloudflare-warp-integration-the-action-parameter-selects-the-operation'
|
||||
- depth: 2
|
||||
title: Manage NordVPN integration. The action parameter selects the operation.
|
||||
url: '#manage-nordvpn-integration-the-action-parameter-selects-the-operation'
|
||||
- depth: 2
|
||||
title: Manage PIA WireGuard integration. The action parameter selects the
|
||||
operation.
|
||||
url: '#manage-pia-wireguard-integration-the-action-parameter-selects-the-operation'
|
||||
- depth: 2
|
||||
title: Reset traffic counters for a specific outbound by tag.
|
||||
url: '#reset-traffic-counters-for-a-specific-outbound-by-tag'
|
||||
- depth: 2
|
||||
title: >-
|
||||
Test an outbound configuration. Sends the outbound JSON (required),
|
||||
title: Test an outbound configuration. Sends the outbound JSON (required),
|
||||
optionally all outbounds (to resolve sockopt.dialerProxy dependencies),
|
||||
and a mode flag.
|
||||
url: >-
|
||||
#test-an-outbound-configuration-sends-the-outbound-json-required-optionally-all-outbounds-to-resolve-sockoptdialerproxy-dependencies-and-a-mode-flag
|
||||
url: '#test-an-outbound-configuration-sends-the-outbound-json-required-optionally-all-outbounds-to-resolve-sockoptdialerproxy-dependencies-and-a-mode-flag'
|
||||
- depth: 2
|
||||
title: >-
|
||||
Test a batch of outbounds (max 50) through one shared temp xray
|
||||
instance. Returns an array of results in input order, each with the
|
||||
outbound tag, delay, HTTP status and a connect/TLS/TTFB timing
|
||||
breakdown.
|
||||
url: >-
|
||||
#test-a-batch-of-outbounds-max-50-through-one-shared-temp-xray-instance-returns-an-array-of-results-in-input-order-each-with-the-outbound-tag-delay-http-status-and-a-connecttlsttfb-timing-breakdown
|
||||
title: Test a batch of outbounds (max 50) through one shared temp xray instance.
|
||||
Returns an array of results in input order, each with the outbound tag,
|
||||
delay, HTTP status and a connect/TLS/TTFB timing breakdown.
|
||||
url: '#test-a-batch-of-outbounds-max-50-through-one-shared-temp-xray-instance-returns-an-array-of-results-in-input-order-each-with-the-outbound-tag-delay-http-status-and-a-connecttlsttfb-timing-breakdown'
|
||||
- depth: 2
|
||||
title: >-
|
||||
Live state of routing balancers in the running core
|
||||
title: 'Live state of routing balancers in the running core
|
||||
(RoutingService.GetBalancerInfo): current override and the targets the
|
||||
strategy prefers. Returns a map keyed by balancer tag.
|
||||
url: >-
|
||||
#live-state-of-routing-balancers-in-the-running-core-routingservicegetbalancerinfo-current-override-and-the-targets-the-strategy-prefers-returns-a-map-keyed-by-balancer-tag
|
||||
strategy prefers. Returns a map keyed by balancer tag.'
|
||||
url: '#live-state-of-routing-balancers-in-the-running-core-routingservicegetbalancerinfo-current-override-and-the-targets-the-strategy-prefers-returns-a-map-keyed-by-balancer-tag'
|
||||
- depth: 2
|
||||
title: >-
|
||||
Force a balancer in the running core to always pick one outbound
|
||||
title: Force a balancer in the running core to always pick one outbound
|
||||
(RoutingService.OverrideBalancerTarget). Applied live without a restart;
|
||||
cleared automatically when Xray restarts.
|
||||
url: >-
|
||||
#force-a-balancer-in-the-running-core-to-always-pick-one-outbound-routingserviceoverridebalancertarget-applied-live-without-a-restart-cleared-automatically-when-xray-restarts
|
||||
url: '#force-a-balancer-in-the-running-core-to-always-pick-one-outbound-routingserviceoverridebalancertarget-applied-live-without-a-restart-cleared-automatically-when-xray-restarts'
|
||||
- depth: 2
|
||||
title: >-
|
||||
Ask the running core which outbound its router would pick for a
|
||||
synthetic connection (RoutingService.TestRoute). No traffic is sent.
|
||||
url: >-
|
||||
#ask-the-running-core-which-outbound-its-router-would-pick-for-a-synthetic-connection-routingservicetestroute-no-traffic-is-sent
|
||||
title: Ask the running core which outbound its router would pick for a synthetic
|
||||
connection (RoutingService.TestRoute). No traffic is sent.
|
||||
url: '#ask-the-running-core-which-outbound-its-router-would-pick-for-a-synthetic-connection-routingservicetestroute-no-traffic-is-sent'
|
||||
- depth: 2
|
||||
title: >-
|
||||
List all outbound subscriptions (remote URLs that supply additional
|
||||
title: List the geo databases (.dat files) in the Xray asset folder, with the
|
||||
layout detected from their contents, size, modification time and
|
||||
category count. A database that fails to parse is still listed, with the
|
||||
reason in "error".
|
||||
url: '#list-the-geo-databases-dat-files-in-the-xray-asset-folder-with-the-layout-detected-from-their-contents-size-modification-time-and-category-count-a-database-that-fails-to-parse-is-still-listed-with-the-reason-in-error'
|
||||
- depth: 2
|
||||
title: One page of a database's categories, each with its entry count and the
|
||||
attributes its domains carry (e.g. "ads", "cn").
|
||||
url: '#one-page-of-a-databases-categories-each-with-its-entry-count-and-the-attributes-its-domains-carry-eg-ads-cn'
|
||||
- depth: 2
|
||||
title: One page of the rules inside a category — domain rules typed as
|
||||
domain/full/keyword/regexp for geosite databases, CIDRs for geoip ones.
|
||||
url: '#one-page-of-the-rules-inside-a-category--domain-rules-typed-as-domainfullkeywordregexp-for-geosite-databases-cidrs-for-geoip-ones'
|
||||
- depth: 2
|
||||
title: 'Check routing tokens against the databases on disk and return only the
|
||||
ones that do not resolve. Plain domains and CIDRs are ignored. Each
|
||||
issue carries a reason: syntax, fileMissing or categoryMissing.'
|
||||
url: '#check-routing-tokens-against-the-databases-on-disk-and-return-only-the-ones-that-do-not-resolve-plain-domains-and-cidrs-are-ignored-each-issue-carries-a-reason-syntax-filemissing-or-categorymissing'
|
||||
- depth: 2
|
||||
title: List all outbound subscriptions (remote URLs that supply additional
|
||||
outbounds), newest first.
|
||||
url: >-
|
||||
#list-all-outbound-subscriptions-remote-urls-that-supply-additional-outbounds-newest-first
|
||||
url: '#list-all-outbound-subscriptions-remote-urls-that-supply-additional-outbounds-newest-first'
|
||||
- depth: 2
|
||||
title: >-
|
||||
Create an outbound subscription. The URL is fetched, parsed into
|
||||
title: Create an outbound subscription. The URL is fetched, parsed into
|
||||
outbounds with stable tags, and merged additively into the running Xray
|
||||
config.
|
||||
url: >-
|
||||
#create-an-outbound-subscription-the-url-is-fetched-parsed-into-outbounds-with-stable-tags-and-merged-additively-into-the-running-xray-config
|
||||
url: '#create-an-outbound-subscription-the-url-is-fetched-parsed-into-outbounds-with-stable-tags-and-merged-additively-into-the-running-xray-config'
|
||||
- depth: 2
|
||||
title: >-
|
||||
Update an existing outbound subscription by id. Accepts the same form
|
||||
title: Update an existing outbound subscription by id. Accepts the same form
|
||||
fields as create.
|
||||
url: >-
|
||||
#update-an-existing-outbound-subscription-by-id-accepts-the-same-form-fields-as-create
|
||||
url: '#update-an-existing-outbound-subscription-by-id-accepts-the-same-form-fields-as-create'
|
||||
- depth: 2
|
||||
title: Delete an outbound subscription by id.
|
||||
url: '#delete-an-outbound-subscription-by-id'
|
||||
- depth: 2
|
||||
title: >-
|
||||
Delete an outbound subscription by id (POST alias of DELETE for
|
||||
axios-friendly clients).
|
||||
url: >-
|
||||
#delete-an-outbound-subscription-by-id-post-alias-of-delete-for-axios-friendly-clients
|
||||
title: Delete an outbound subscription by id (POST alias of DELETE for clients
|
||||
that cannot send DELETE).
|
||||
url: '#delete-an-outbound-subscription-by-id-post-alias-of-delete-for-clients-that-cannot-send-delete'
|
||||
- depth: 2
|
||||
title: >-
|
||||
Force an immediate re-fetch of the subscription and return the parsed
|
||||
title: Force an immediate re-fetch of the subscription and return the parsed
|
||||
outbounds. Signals Xray to reload.
|
||||
url: >-
|
||||
#force-an-immediate-re-fetch-of-the-subscription-and-return-the-parsed-outbounds-signals-xray-to-reload
|
||||
url: '#force-an-immediate-re-fetch-of-the-subscription-and-return-the-parsed-outbounds-signals-xray-to-reload'
|
||||
- depth: 2
|
||||
title: >-
|
||||
Reorder a subscription one step up or down in priority (controls its
|
||||
title: Reorder a subscription one step up or down in priority (controls its
|
||||
position in the merged outbounds).
|
||||
url: >-
|
||||
#reorder-a-subscription-one-step-up-or-down-in-priority-controls-its-position-in-the-merged-outbounds
|
||||
url: '#reorder-a-subscription-one-step-up-or-down-in-priority-controls-its-position-in-the-merged-outbounds'
|
||||
- depth: 2
|
||||
title: >-
|
||||
Preview a subscription URL: fetch and parse it into outbounds without
|
||||
persisting anything.
|
||||
url: >-
|
||||
#preview-a-subscription-url-fetch-and-parse-it-into-outbounds-without-persisting-anything
|
||||
title: 'Preview a subscription URL: fetch and parse it into outbounds without
|
||||
persisting anything.'
|
||||
url: '#preview-a-subscription-url-fetch-and-parse-it-into-outbounds-without-persisting-anything'
|
||||
structuredData:
|
||||
headings:
|
||||
- content: >-
|
||||
Return the Xray config template (JSON string), available inbound tags,
|
||||
- content: Return the Xray config template (JSON string), available inbound tags,
|
||||
client reverse tags, and the configured outbound test URL in one
|
||||
response.
|
||||
id: >-
|
||||
return-the-xray-config-template-json-string-available-inbound-tags-client-reverse-tags-and-the-configured-outbound-test-url-in-one-response
|
||||
- content: >-
|
||||
Return the built-in default Xray config shipped with the panel
|
||||
id: return-the-xray-config-template-json-string-available-inbound-tags-client-reverse-tags-and-the-configured-outbound-test-url-in-one-response
|
||||
- content: Return the built-in default Xray config shipped with the panel
|
||||
(identical to /panel/api/setting/getDefaultJsonConfig).
|
||||
id: >-
|
||||
return-the-built-in-default-xray-config-shipped-with-the-panel-identical-to-panelapisettinggetdefaultjsonconfig
|
||||
- content: >-
|
||||
Return traffic statistics for every outbound. Each outbound shows
|
||||
id: return-the-built-in-default-xray-config-shipped-with-the-panel-identical-to-panelapisettinggetdefaultjsonconfig
|
||||
- content: Return traffic statistics for every outbound. Each outbound shows
|
||||
up/down/total counters.
|
||||
id: >-
|
||||
return-traffic-statistics-for-every-outbound-each-outbound-shows-updowntotal-counters
|
||||
- content: >-
|
||||
Return the most recent Xray process stdout/stderr output. Useful to
|
||||
id: return-traffic-statistics-for-every-outbound-each-outbound-shows-updowntotal-counters
|
||||
- content: Return the most recent Xray process stdout/stderr output. Useful to
|
||||
check for startup errors or runtime warnings.
|
||||
id: >-
|
||||
return-the-most-recent-xray-process-stdoutstderr-output-useful-to-check-for-startup-errors-or-runtime-warnings
|
||||
- content: >-
|
||||
Save the Xray JSON config template and optionally the outbound test
|
||||
id: return-the-most-recent-xray-process-stdoutstderr-output-useful-to-check-for-startup-errors-or-runtime-warnings
|
||||
- content: Save the Xray JSON config template and optionally the outbound test
|
||||
URL. Both are sent as form fields.
|
||||
id: >-
|
||||
save-the-xray-json-config-template-and-optionally-the-outbound-test-url-both-are-sent-as-form-fields
|
||||
- content: >-
|
||||
Manage Cloudflare Warp integration. The action parameter selects the
|
||||
operation.
|
||||
id: >-
|
||||
manage-cloudflare-warp-integration-the-action-parameter-selects-the-operation
|
||||
- content: >-
|
||||
Manage NordVPN integration. The action parameter selects the
|
||||
id: save-the-xray-json-config-template-and-optionally-the-outbound-test-url-both-are-sent-as-form-fields
|
||||
- content: Manage Cloudflare Warp integration. The action parameter selects the
|
||||
operation.
|
||||
id: manage-cloudflare-warp-integration-the-action-parameter-selects-the-operation
|
||||
- content: Manage NordVPN integration. The action parameter selects the operation.
|
||||
id: manage-nordvpn-integration-the-action-parameter-selects-the-operation
|
||||
- content: Manage PIA WireGuard integration. The action parameter selects the
|
||||
operation.
|
||||
id: manage-pia-wireguard-integration-the-action-parameter-selects-the-operation
|
||||
- content: Reset traffic counters for a specific outbound by tag.
|
||||
id: reset-traffic-counters-for-a-specific-outbound-by-tag
|
||||
- content: >-
|
||||
Test an outbound configuration. Sends the outbound JSON (required),
|
||||
- content: Test an outbound configuration. Sends the outbound JSON (required),
|
||||
optionally all outbounds (to resolve sockopt.dialerProxy
|
||||
dependencies), and a mode flag.
|
||||
id: >-
|
||||
test-an-outbound-configuration-sends-the-outbound-json-required-optionally-all-outbounds-to-resolve-sockoptdialerproxy-dependencies-and-a-mode-flag
|
||||
- content: >-
|
||||
Test a batch of outbounds (max 50) through one shared temp xray
|
||||
id: test-an-outbound-configuration-sends-the-outbound-json-required-optionally-all-outbounds-to-resolve-sockoptdialerproxy-dependencies-and-a-mode-flag
|
||||
- content: Test a batch of outbounds (max 50) through one shared temp xray
|
||||
instance. Returns an array of results in input order, each with the
|
||||
outbound tag, delay, HTTP status and a connect/TLS/TTFB timing
|
||||
breakdown.
|
||||
id: >-
|
||||
test-a-batch-of-outbounds-max-50-through-one-shared-temp-xray-instance-returns-an-array-of-results-in-input-order-each-with-the-outbound-tag-delay-http-status-and-a-connecttlsttfb-timing-breakdown
|
||||
- content: >-
|
||||
Live state of routing balancers in the running core
|
||||
id: test-a-batch-of-outbounds-max-50-through-one-shared-temp-xray-instance-returns-an-array-of-results-in-input-order-each-with-the-outbound-tag-delay-http-status-and-a-connecttlsttfb-timing-breakdown
|
||||
- content: 'Live state of routing balancers in the running core
|
||||
(RoutingService.GetBalancerInfo): current override and the targets the
|
||||
strategy prefers. Returns a map keyed by balancer tag.
|
||||
id: >-
|
||||
live-state-of-routing-balancers-in-the-running-core-routingservicegetbalancerinfo-current-override-and-the-targets-the-strategy-prefers-returns-a-map-keyed-by-balancer-tag
|
||||
- content: >-
|
||||
Force a balancer in the running core to always pick one outbound
|
||||
strategy prefers. Returns a map keyed by balancer tag.'
|
||||
id: live-state-of-routing-balancers-in-the-running-core-routingservicegetbalancerinfo-current-override-and-the-targets-the-strategy-prefers-returns-a-map-keyed-by-balancer-tag
|
||||
- content: Force a balancer in the running core to always pick one outbound
|
||||
(RoutingService.OverrideBalancerTarget). Applied live without a
|
||||
restart; cleared automatically when Xray restarts.
|
||||
id: >-
|
||||
force-a-balancer-in-the-running-core-to-always-pick-one-outbound-routingserviceoverridebalancertarget-applied-live-without-a-restart-cleared-automatically-when-xray-restarts
|
||||
- content: >-
|
||||
Ask the running core which outbound its router would pick for a
|
||||
id: force-a-balancer-in-the-running-core-to-always-pick-one-outbound-routingserviceoverridebalancertarget-applied-live-without-a-restart-cleared-automatically-when-xray-restarts
|
||||
- content: Ask the running core which outbound its router would pick for a
|
||||
synthetic connection (RoutingService.TestRoute). No traffic is sent.
|
||||
id: >-
|
||||
ask-the-running-core-which-outbound-its-router-would-pick-for-a-synthetic-connection-routingservicetestroute-no-traffic-is-sent
|
||||
- content: >-
|
||||
List all outbound subscriptions (remote URLs that supply additional
|
||||
id: ask-the-running-core-which-outbound-its-router-would-pick-for-a-synthetic-connection-routingservicetestroute-no-traffic-is-sent
|
||||
- content: List the geo databases (.dat files) in the Xray asset folder, with the
|
||||
layout detected from their contents, size, modification time and
|
||||
category count. A database that fails to parse is still listed, with
|
||||
the reason in "error".
|
||||
id: list-the-geo-databases-dat-files-in-the-xray-asset-folder-with-the-layout-detected-from-their-contents-size-modification-time-and-category-count-a-database-that-fails-to-parse-is-still-listed-with-the-reason-in-error
|
||||
- content: One page of a database's categories, each with its entry count and the
|
||||
attributes its domains carry (e.g. "ads", "cn").
|
||||
id: one-page-of-a-databases-categories-each-with-its-entry-count-and-the-attributes-its-domains-carry-eg-ads-cn
|
||||
- content: One page of the rules inside a category — domain rules typed as
|
||||
domain/full/keyword/regexp for geosite databases, CIDRs for geoip
|
||||
ones.
|
||||
id: one-page-of-the-rules-inside-a-category--domain-rules-typed-as-domainfullkeywordregexp-for-geosite-databases-cidrs-for-geoip-ones
|
||||
- content: 'Check routing tokens against the databases on disk and return only the
|
||||
ones that do not resolve. Plain domains and CIDRs are ignored. Each
|
||||
issue carries a reason: syntax, fileMissing or categoryMissing.'
|
||||
id: check-routing-tokens-against-the-databases-on-disk-and-return-only-the-ones-that-do-not-resolve-plain-domains-and-cidrs-are-ignored-each-issue-carries-a-reason-syntax-filemissing-or-categorymissing
|
||||
- content: List all outbound subscriptions (remote URLs that supply additional
|
||||
outbounds), newest first.
|
||||
id: >-
|
||||
list-all-outbound-subscriptions-remote-urls-that-supply-additional-outbounds-newest-first
|
||||
- content: >-
|
||||
Create an outbound subscription. The URL is fetched, parsed into
|
||||
id: list-all-outbound-subscriptions-remote-urls-that-supply-additional-outbounds-newest-first
|
||||
- content: Create an outbound subscription. The URL is fetched, parsed into
|
||||
outbounds with stable tags, and merged additively into the running
|
||||
Xray config.
|
||||
id: >-
|
||||
create-an-outbound-subscription-the-url-is-fetched-parsed-into-outbounds-with-stable-tags-and-merged-additively-into-the-running-xray-config
|
||||
- content: >-
|
||||
Update an existing outbound subscription by id. Accepts the same form
|
||||
id: create-an-outbound-subscription-the-url-is-fetched-parsed-into-outbounds-with-stable-tags-and-merged-additively-into-the-running-xray-config
|
||||
- content: Update an existing outbound subscription by id. Accepts the same form
|
||||
fields as create.
|
||||
id: >-
|
||||
update-an-existing-outbound-subscription-by-id-accepts-the-same-form-fields-as-create
|
||||
id: update-an-existing-outbound-subscription-by-id-accepts-the-same-form-fields-as-create
|
||||
- content: Delete an outbound subscription by id.
|
||||
id: delete-an-outbound-subscription-by-id
|
||||
- content: >-
|
||||
Delete an outbound subscription by id (POST alias of DELETE for
|
||||
axios-friendly clients).
|
||||
id: >-
|
||||
delete-an-outbound-subscription-by-id-post-alias-of-delete-for-axios-friendly-clients
|
||||
- content: >-
|
||||
Force an immediate re-fetch of the subscription and return the parsed
|
||||
- content: Delete an outbound subscription by id (POST alias of DELETE for clients
|
||||
that cannot send DELETE).
|
||||
id: delete-an-outbound-subscription-by-id-post-alias-of-delete-for-clients-that-cannot-send-delete
|
||||
- content: Force an immediate re-fetch of the subscription and return the parsed
|
||||
outbounds. Signals Xray to reload.
|
||||
id: >-
|
||||
force-an-immediate-re-fetch-of-the-subscription-and-return-the-parsed-outbounds-signals-xray-to-reload
|
||||
- content: >-
|
||||
Reorder a subscription one step up or down in priority (controls its
|
||||
id: force-an-immediate-re-fetch-of-the-subscription-and-return-the-parsed-outbounds-signals-xray-to-reload
|
||||
- content: Reorder a subscription one step up or down in priority (controls its
|
||||
position in the merged outbounds).
|
||||
id: >-
|
||||
reorder-a-subscription-one-step-up-or-down-in-priority-controls-its-position-in-the-merged-outbounds
|
||||
- content: >-
|
||||
Preview a subscription URL: fetch and parse it into outbounds without
|
||||
persisting anything.
|
||||
id: >-
|
||||
preview-a-subscription-url-fetch-and-parse-it-into-outbounds-without-persisting-anything
|
||||
id: reorder-a-subscription-one-step-up-or-down-in-priority-controls-its-position-in-the-merged-outbounds
|
||||
- content: 'Preview a subscription URL: fetch and parse it into outbounds without
|
||||
persisting anything.'
|
||||
id: preview-a-subscription-url-fetch-and-parse-it-into-outbounds-without-persisting-anything
|
||||
contents: []
|
||||
---
|
||||
|
||||
@@ -251,7 +217,7 @@ export default function Layout(props) {
|
||||
return (
|
||||
<>
|
||||
{props.children}
|
||||
<Comp document="./public/openapi.json" webhooks={[]} operations={[{"path":"/panel/api/xray/","method":"post"},{"path":"/panel/api/xray/getDefaultJsonConfig","method":"get"},{"path":"/panel/api/xray/getOutboundsTraffic","method":"get"},{"path":"/panel/api/xray/getXrayResult","method":"get"},{"path":"/panel/api/xray/update","method":"post"},{"path":"/panel/api/xray/warp/{action}","method":"post"},{"path":"/panel/api/xray/nord/{action}","method":"post"},{"path":"/panel/api/xray/resetOutboundsTraffic","method":"post"},{"path":"/panel/api/xray/testOutbound","method":"post"},{"path":"/panel/api/xray/testOutbounds","method":"post"},{"path":"/panel/api/xray/balancerStatus","method":"post"},{"path":"/panel/api/xray/balancerOverride","method":"post"},{"path":"/panel/api/xray/routeTest","method":"post"},{"path":"/panel/api/xray/outbound-subs","method":"get"},{"path":"/panel/api/xray/outbound-subs","method":"post"},{"path":"/panel/api/xray/outbound-subs/{id}","method":"post"},{"path":"/panel/api/xray/outbound-subs/{id}","method":"delete"},{"path":"/panel/api/xray/outbound-subs/{id}/del","method":"post"},{"path":"/panel/api/xray/outbound-subs/{id}/refresh","method":"post"},{"path":"/panel/api/xray/outbound-subs/{id}/move","method":"post"},{"path":"/panel/api/xray/outbound-subs/parse","method":"post"}]} showTitle />
|
||||
<Comp document="./public/openapi.json" webhooks={[]} operations={[{"path":"/panel/api/xray/","method":"post"},{"path":"/panel/api/xray/getDefaultJsonConfig","method":"get"},{"path":"/panel/api/xray/getOutboundsTraffic","method":"get"},{"path":"/panel/api/xray/getXrayResult","method":"get"},{"path":"/panel/api/xray/update","method":"post"},{"path":"/panel/api/xray/warp/{action}","method":"post"},{"path":"/panel/api/xray/nord/{action}","method":"post"},{"path":"/panel/api/xray/pia/{action}","method":"post"},{"path":"/panel/api/xray/resetOutboundsTraffic","method":"post"},{"path":"/panel/api/xray/testOutbound","method":"post"},{"path":"/panel/api/xray/testOutbounds","method":"post"},{"path":"/panel/api/xray/balancerStatus","method":"post"},{"path":"/panel/api/xray/balancerOverride","method":"post"},{"path":"/panel/api/xray/routeTest","method":"post"},{"path":"/panel/api/xray/geodata/files","method":"get"},{"path":"/panel/api/xray/geodata/categories","method":"get"},{"path":"/panel/api/xray/geodata/entries","method":"get"},{"path":"/panel/api/xray/geodata/validate","method":"post"},{"path":"/panel/api/xray/outbound-subs","method":"get"},{"path":"/panel/api/xray/outbound-subs","method":"post"},{"path":"/panel/api/xray/outbound-subs/{id}","method":"post"},{"path":"/panel/api/xray/outbound-subs/{id}","method":"delete"},{"path":"/panel/api/xray/outbound-subs/{id}/del","method":"post"},{"path":"/panel/api/xray/outbound-subs/{id}/refresh","method":"post"},{"path":"/panel/api/xray/outbound-subs/{id}/move","method":"post"},{"path":"/panel/api/xray/outbound-subs/parse","method":"post"}]} showTitle />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,11 +1,12 @@
|
||||
---
|
||||
title: خروجیها و مسیریابی
|
||||
description: مدیریت ترافیک خروجی در 3x-ui — خروجیهای WARP و NordVPN، اشتراکهای خروجی (مجموعه سرورها)، قواعد مسیریابی و متعادلکنندههای بار.
|
||||
description: مدیریت ترافیک خروجی در 3x-ui — خروجیهای WARP، NordVPN، WireGuard PIA، اشتراکهای خروجی (مجموعه سرورها)، قواعد مسیریابی و متعادلکنندههای بار.
|
||||
icon: Route
|
||||
---
|
||||
|
||||
ورودیها کلاینتها را میپذیرند؛ **خروجیها** تعیین میکنند ترافیک آنها در ادامه به کجا برود.
|
||||
3x-ui میتواند ترافیک را از طریق Cloudflare WARP، NordVPN یا مجموعههای خروجی دلخواه
|
||||
3x-ui میتواند ترافیک را از طریق Cloudflare WARP، NordVPN، Private Internet Access
|
||||
(خروجی WireGuard) یا مجموعههای خروجی دلخواه
|
||||
واردشده از یک اشتراک مسیریابی کند و با قواعد مسیریابی و متعادلکنندهها میان آنها
|
||||
انتخاب نماید.
|
||||
|
||||
@@ -86,6 +87,23 @@ WARP به سرور شما امکان میدهد ترافیک خود را از
|
||||
یک کلید خصوصی را مستقیماً بپذیرد) و کشورها/سرورها را فهرست کند تا بتوانید یک خروجی NordVPN
|
||||
بسازید.
|
||||
|
||||
## خروجی WireGuard PIA
|
||||
|
||||
3x-ui میتواند با نام کاربری و رمز عبور PIA وارد شود، کشورها/منطقهها/سرورها را
|
||||
از فهرست امضاشده نشان دهد و یک خروجی WireGuard بسازد. از
|
||||
**Xray → خروجیها → بیشتر → PIA** وارد شوید، سرور را انتخاب کنید و خروجی را
|
||||
اضافه کنید. میتوان چند سرور افزود (هر hostname یک خروجی). برچسب
|
||||
`pia-<region>-<server>` است (مثلاً `pia-us-east-useast1`). افزودن یا **Reset**
|
||||
در هر ردیف کلید را با `/addKey` ثبت میکند. یک hostname را نمیتوان دو بار
|
||||
افزود. خروج فقط توکن ذخیرهشده را پاک میکند؛ حذف خروجی از فهرست خروجیها.
|
||||
Reset یا حذف، peer مربوط به WireGuard را در حساب PIA باطل نمیکند.
|
||||
|
||||
گذرواژه ذخیره نمیشود. توکن API مربوط به PIA با همان تنظیم
|
||||
`NODE_TOKEN_ENCRYPTION` گرهها ذخیره میشود. اگر کلید قدیمی
|
||||
`XUI_NODE_TOKEN_KEY` را بدون ورود دوباره به PIA کنار بگذارید، Add/Reset
|
||||
تا ورود مجدد شکست میخورد. `allowedIPs` فقط
|
||||
`0.0.0.0/0` است.
|
||||
|
||||
## اشتراکهای خروجی (مجموعه سرورها)
|
||||
|
||||
یک **اشتراک خروجی** یک اشتراک share-link از راه دور را وارد میکند و سرورهای آن را بهعنوان
|
||||
|
||||
@@ -37,11 +37,10 @@ _openapi:
|
||||
- depth: 2
|
||||
title: >-
|
||||
Create a new client and attach it to one or more inbounds in a single
|
||||
call. Body is JSON. Per-protocol secrets (UUID for VLESS/VMess, password
|
||||
for Trojan/Shadowsocks, auth for Hysteria) are generated server-side
|
||||
when omitted, so callers can send only the universal fields.
|
||||
call. Body is JSON. Per-protocol secrets are generated server-side when
|
||||
omitted, so callers can send only the universal fields.
|
||||
url: >-
|
||||
#create-a-new-client-and-attach-it-to-one-or-more-inbounds-in-a-single-call-body-is-json-per-protocol-secrets-uuid-for-vlessvmess-password-for-trojanshadowsocks-auth-for-hysteria-are-generated-server-side-when-omitted-so-callers-can-send-only-the-universal-fields
|
||||
#create-a-new-client-and-attach-it-to-one-or-more-inbounds-in-a-single-call-body-is-json-per-protocol-secrets-are-generated-server-side-when-omitted-so-callers-can-send-only-the-universal-fields
|
||||
- depth: 2
|
||||
title: >-
|
||||
Update an existing client by email. Changes propagate to every attached
|
||||
@@ -352,12 +351,10 @@ _openapi:
|
||||
fetch-one-client-by-email-including-the-inbound-ids-and-external-config-ids-it-is-attached-to
|
||||
- content: >-
|
||||
Create a new client and attach it to one or more inbounds in a single
|
||||
call. Body is JSON. Per-protocol secrets (UUID for VLESS/VMess,
|
||||
password for Trojan/Shadowsocks, auth for Hysteria) are generated
|
||||
server-side when omitted, so callers can send only the universal
|
||||
fields.
|
||||
call. Body is JSON. Per-protocol secrets are generated server-side
|
||||
when omitted, so callers can send only the universal fields.
|
||||
id: >-
|
||||
create-a-new-client-and-attach-it-to-one-or-more-inbounds-in-a-single-call-body-is-json-per-protocol-secrets-uuid-for-vlessvmess-password-for-trojanshadowsocks-auth-for-hysteria-are-generated-server-side-when-omitted-so-callers-can-send-only-the-universal-fields
|
||||
create-a-new-client-and-attach-it-to-one-or-more-inbounds-in-a-single-call-body-is-json-per-protocol-secrets-are-generated-server-side-when-omitted-so-callers-can-send-only-the-universal-fields
|
||||
- content: >-
|
||||
Update an existing client by email. Changes propagate to every
|
||||
attached inbound. Body is the JSON client payload — supply the full
|
||||
@@ -610,7 +607,57 @@ _openapi:
|
||||
dokodemo, tunnel) contribute nothing.
|
||||
id: >-
|
||||
return-every-url-for-one-client-across-all-attached-inbounds--the-same-strings-the-copy-url-button-copies-in-the-panel-ui-supported-protocols-vmess-vless-trojan-shadowsocks-hysteria-if-streamsettingsexternalproxy-is-set-returns-one-url-per-external-proxy-protocols-without-a-url-form-socks-http-mixed-wireguard-dokodemo-tunnel-contribute-nothing
|
||||
contents: []
|
||||
contents:
|
||||
- content: >-
|
||||
Fields the server fills in when they are omitted — a valid value sent
|
||||
by the caller is never overwritten. Re-adding an email that already
|
||||
exists, with its stored `subId`, reuses the stored `id`, `password`,
|
||||
`auth` and `secret` instead of minting new ones, so the identity stays
|
||||
in sync across its inbounds.
|
||||
|
||||
|
||||
- **VLESS / VMess** — `id`, a fresh UUID
|
||||
|
||||
- **Trojan** — `password`
|
||||
|
||||
- **Shadowsocks** — `password`. On a `2022-blake3-*` inbound a
|
||||
supplied password that does not base64-decode to the key length of the
|
||||
cipher (16 or 32 bytes) is replaced by a generated key and the call
|
||||
still succeeds, so read the client back if you did not let the server
|
||||
pick. Legacy ciphers keep any non-empty password
|
||||
|
||||
- **Hysteria** — `auth`
|
||||
|
||||
- **mtproto** — `secret`, a FakeTLS secret derived from the fronting
|
||||
domain of the inbound, or from `www.cloudflare.com` when it has none
|
||||
|
||||
- **WireGuard** — `privateKey` and `publicKey` when both are blank, or
|
||||
`publicKey` alone when only a `privateKey` was sent, plus
|
||||
`allowedIPs`: one free `/32` taken from the /24 the existing peers of
|
||||
that inbound already sit in, or from `10.0.0.0/24` when it has none
|
||||
|
||||
|
||||
Accepted on the same body but never generated: `preSharedKey` and
|
||||
`keepAlive` (WireGuard), `adTag` (mtproto).
|
||||
|
||||
|
||||
WireGuard is the only one of these that can fail. Allocation widens
|
||||
the search to the containing /16 before giving up with `wireguard: no
|
||||
free address available in <scope>`, and an `allowedIPs` supplied by
|
||||
the caller is validated instead of allocated: `wireguard: allowedIPs
|
||||
entry already used by another client: <address>` when a different
|
||||
client of that same inbound already holds it. The check is per
|
||||
inbound, so the same address on two different inbounds is accepted.
|
||||
The same validation runs on POST /panel/api/clients/{email}/attach,
|
||||
where a client that already carries an address brings it along.
|
||||
heading: create-a-new-client-and-attach-it-to-one-or-more-inbounds-in-a-single-call-body-is-json-per-protocol-secrets-are-generated-server-side-when-omitted-so-callers-can-send-only-the-universal-fields
|
||||
- content: 'A WireGuard client brings its stored `allowedIPs` into the new inbound
|
||||
instead of being given a fresh address, so the call fails with
|
||||
`wireguard: allowedIPs entry already used by another client:
|
||||
<address>` when a different client of the target inbound already holds
|
||||
it. Free the address on that inbound first — see POST
|
||||
/panel/api/clients/add for the full rule.'
|
||||
heading: attach-an-existing-client-to-one-or-more-additional-inbounds-body-is-json
|
||||
---
|
||||
|
||||
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
|
||||
|
||||
@@ -102,7 +102,7 @@ export default function Layout(props) {
|
||||
return (
|
||||
<>
|
||||
{props.children}
|
||||
<Comp document="./public/openapi.json" webhooks={[]} operations={[{"path":"/panel/api/hosts/list","method":"get"},{"path":"/panel/api/hosts/get/{id}","method":"get"},{"path":"/panel/api/hosts/byInbound/{inboundId}","method":"get"},{"path":"/panel/api/hosts/tags","method":"get"},{"path":"/panel/api/hosts/add","method":"post"},{"path":"/panel/api/hosts/update/{id}","method":"post"},{"path":"/panel/api/hosts/del/{id}","method":"post"},{"path":"/panel/api/hosts/setEnable/{id}","method":"post"},{"path":"/panel/api/hosts/reorder","method":"post"},{"path":"/panel/api/hosts/bulk/setEnable","method":"post"},{"path":"/panel/api/hosts/bulk/del","method":"post"}]} showTitle />
|
||||
<Comp document="./public/openapi.json" webhooks={[]} operations={[{"path":"/panel/api/hosts/list","method":"get"},{"path":"/panel/api/hosts/get/{groupId}","method":"get"},{"path":"/panel/api/hosts/byInbound/{inboundId}","method":"get"},{"path":"/panel/api/hosts/tags","method":"get"},{"path":"/panel/api/hosts/add","method":"post"},{"path":"/panel/api/hosts/update/{groupId}","method":"post"},{"path":"/panel/api/hosts/del/{groupId}","method":"post"},{"path":"/panel/api/hosts/setEnable/{groupId}","method":"post"},{"path":"/panel/api/hosts/reorder","method":"post"},{"path":"/panel/api/hosts/bulk/setEnable","method":"post"},{"path":"/panel/api/hosts/bulk/del","method":"post"}]} showTitle />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,12 +1,13 @@
|
||||
---
|
||||
title: Исходящие соединения и маршрутизация
|
||||
description: Управляйте исходящим трафиком в 3x-ui — outbound-соединения WARP и NordVPN, подписки на исходящие соединения (пулы серверов), правила маршрутизации и балансировщики нагрузки.
|
||||
description: Управляйте исходящим трафиком в 3x-ui — WARP, NordVPN, PIA WireGuard, подписки на исходящие соединения (пулы серверов), правила маршрутизации и балансировщики нагрузки.
|
||||
icon: Route
|
||||
---
|
||||
|
||||
Inbound-соединения принимают клиентов; **outbound-соединения** определяют, куда
|
||||
дальше пойдёт их трафик. 3x-ui может направлять трафик через Cloudflare WARP,
|
||||
NordVPN или произвольные пулы исходящих соединений, импортированные из подписки,
|
||||
NordVPN, Private Internet Access (WireGuard) или произвольные пулы
|
||||
исходящих соединений, импортированные из подписки,
|
||||
а также выбирать между ними с помощью правил маршрутизации и балансировщиков.
|
||||
|
||||
## Редактирование исходящих соединений и маршрутизации
|
||||
@@ -93,6 +94,24 @@ WARP. Также можно применить бесплатную лиценз
|
||||
(или принимать приватный ключ напрямую) и выводить список стран/серверов, чтобы вы
|
||||
могли построить outbound-соединение NordVPN.
|
||||
|
||||
## PIA WireGuard
|
||||
|
||||
3x-ui может войти с именем пользователя и паролем PIA, показать
|
||||
страны/регионы/серверы из подписанного списка и собрать WireGuard-исходящее.
|
||||
Откройте **Xray → Исходящие → Ещё → PIA**, войдите, выберите сервер и добавьте
|
||||
исходящее. Можно добавить несколько серверов (по одному исходящему на hostname).
|
||||
Тег: `pia-<region>-<server>` (например `pia-us-east-useast1`). Добавление или
|
||||
**Reset** в строке регистрирует ключ через PIA `/addKey`. Один и тот же hostname
|
||||
нельзя добавить дважды. Выход очищает только сохранённый токен; удаляйте
|
||||
исходящие в списке исходящих. Reset и удаление не отзывают WireGuard-peer
|
||||
в аккаунте PIA.
|
||||
|
||||
Пароль не сохраняется. Токен PIA API хранится с той же настройкой
|
||||
`NODE_TOKEN_ENCRYPTION`, что и токены API узлов. Если убрать старый
|
||||
`XUI_NODE_TOKEN_KEY` без повторного входа в PIA, Add/Reset не будут
|
||||
работать, пока вы не войдёте снова. `allowedIPs` только
|
||||
`0.0.0.0/0`.
|
||||
|
||||
## Подписки на исходящие соединения (пулы серверов)
|
||||
|
||||
**Подписка на исходящие соединения** импортирует удалённую подписку со
|
||||
|
||||
@@ -37,11 +37,10 @@ _openapi:
|
||||
- depth: 2
|
||||
title: >-
|
||||
Create a new client and attach it to one or more inbounds in a single
|
||||
call. Body is JSON. Per-protocol secrets (UUID for VLESS/VMess, password
|
||||
for Trojan/Shadowsocks, auth for Hysteria) are generated server-side
|
||||
when omitted, so callers can send only the universal fields.
|
||||
call. Body is JSON. Per-protocol secrets are generated server-side when
|
||||
omitted, so callers can send only the universal fields.
|
||||
url: >-
|
||||
#create-a-new-client-and-attach-it-to-one-or-more-inbounds-in-a-single-call-body-is-json-per-protocol-secrets-uuid-for-vlessvmess-password-for-trojanshadowsocks-auth-for-hysteria-are-generated-server-side-when-omitted-so-callers-can-send-only-the-universal-fields
|
||||
#create-a-new-client-and-attach-it-to-one-or-more-inbounds-in-a-single-call-body-is-json-per-protocol-secrets-are-generated-server-side-when-omitted-so-callers-can-send-only-the-universal-fields
|
||||
- depth: 2
|
||||
title: >-
|
||||
Update an existing client by email. Changes propagate to every attached
|
||||
@@ -352,12 +351,10 @@ _openapi:
|
||||
fetch-one-client-by-email-including-the-inbound-ids-and-external-config-ids-it-is-attached-to
|
||||
- content: >-
|
||||
Create a new client and attach it to one or more inbounds in a single
|
||||
call. Body is JSON. Per-protocol secrets (UUID for VLESS/VMess,
|
||||
password for Trojan/Shadowsocks, auth for Hysteria) are generated
|
||||
server-side when omitted, so callers can send only the universal
|
||||
fields.
|
||||
call. Body is JSON. Per-protocol secrets are generated server-side
|
||||
when omitted, so callers can send only the universal fields.
|
||||
id: >-
|
||||
create-a-new-client-and-attach-it-to-one-or-more-inbounds-in-a-single-call-body-is-json-per-protocol-secrets-uuid-for-vlessvmess-password-for-trojanshadowsocks-auth-for-hysteria-are-generated-server-side-when-omitted-so-callers-can-send-only-the-universal-fields
|
||||
create-a-new-client-and-attach-it-to-one-or-more-inbounds-in-a-single-call-body-is-json-per-protocol-secrets-are-generated-server-side-when-omitted-so-callers-can-send-only-the-universal-fields
|
||||
- content: >-
|
||||
Update an existing client by email. Changes propagate to every
|
||||
attached inbound. Body is the JSON client payload — supply the full
|
||||
@@ -610,7 +607,57 @@ _openapi:
|
||||
dokodemo, tunnel) contribute nothing.
|
||||
id: >-
|
||||
return-every-url-for-one-client-across-all-attached-inbounds--the-same-strings-the-copy-url-button-copies-in-the-panel-ui-supported-protocols-vmess-vless-trojan-shadowsocks-hysteria-if-streamsettingsexternalproxy-is-set-returns-one-url-per-external-proxy-protocols-without-a-url-form-socks-http-mixed-wireguard-dokodemo-tunnel-contribute-nothing
|
||||
contents: []
|
||||
contents:
|
||||
- content: >-
|
||||
Fields the server fills in when they are omitted — a valid value sent
|
||||
by the caller is never overwritten. Re-adding an email that already
|
||||
exists, with its stored `subId`, reuses the stored `id`, `password`,
|
||||
`auth` and `secret` instead of minting new ones, so the identity stays
|
||||
in sync across its inbounds.
|
||||
|
||||
|
||||
- **VLESS / VMess** — `id`, a fresh UUID
|
||||
|
||||
- **Trojan** — `password`
|
||||
|
||||
- **Shadowsocks** — `password`. On a `2022-blake3-*` inbound a
|
||||
supplied password that does not base64-decode to the key length of the
|
||||
cipher (16 or 32 bytes) is replaced by a generated key and the call
|
||||
still succeeds, so read the client back if you did not let the server
|
||||
pick. Legacy ciphers keep any non-empty password
|
||||
|
||||
- **Hysteria** — `auth`
|
||||
|
||||
- **mtproto** — `secret`, a FakeTLS secret derived from the fronting
|
||||
domain of the inbound, or from `www.cloudflare.com` when it has none
|
||||
|
||||
- **WireGuard** — `privateKey` and `publicKey` when both are blank, or
|
||||
`publicKey` alone when only a `privateKey` was sent, plus
|
||||
`allowedIPs`: one free `/32` taken from the /24 the existing peers of
|
||||
that inbound already sit in, or from `10.0.0.0/24` when it has none
|
||||
|
||||
|
||||
Accepted on the same body but never generated: `preSharedKey` and
|
||||
`keepAlive` (WireGuard), `adTag` (mtproto).
|
||||
|
||||
|
||||
WireGuard is the only one of these that can fail. Allocation widens
|
||||
the search to the containing /16 before giving up with `wireguard: no
|
||||
free address available in <scope>`, and an `allowedIPs` supplied by
|
||||
the caller is validated instead of allocated: `wireguard: allowedIPs
|
||||
entry already used by another client: <address>` when a different
|
||||
client of that same inbound already holds it. The check is per
|
||||
inbound, so the same address on two different inbounds is accepted.
|
||||
The same validation runs on POST /panel/api/clients/{email}/attach,
|
||||
where a client that already carries an address brings it along.
|
||||
heading: create-a-new-client-and-attach-it-to-one-or-more-inbounds-in-a-single-call-body-is-json-per-protocol-secrets-are-generated-server-side-when-omitted-so-callers-can-send-only-the-universal-fields
|
||||
- content: 'A WireGuard client brings its stored `allowedIPs` into the new inbound
|
||||
instead of being given a fresh address, so the call fails with
|
||||
`wireguard: allowedIPs entry already used by another client:
|
||||
<address>` when a different client of the target inbound already holds
|
||||
it. Free the address on that inbound first — see POST
|
||||
/panel/api/clients/add for the full rule.'
|
||||
heading: attach-an-existing-client-to-one-or-more-additional-inbounds-body-is-json
|
||||
---
|
||||
|
||||
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
|
||||
|
||||
@@ -103,7 +103,7 @@ export default function Layout(props) {
|
||||
return (
|
||||
<>
|
||||
{props.children}
|
||||
<Comp document="./public/openapi.json" webhooks={[]} operations={[{"path":"/panel/api/hosts/list","method":"get"},{"path":"/panel/api/hosts/get/{id}","method":"get"},{"path":"/panel/api/hosts/byInbound/{inboundId}","method":"get"},{"path":"/panel/api/hosts/tags","method":"get"},{"path":"/panel/api/hosts/add","method":"post"},{"path":"/panel/api/hosts/update/{id}","method":"post"},{"path":"/panel/api/hosts/del/{id}","method":"post"},{"path":"/panel/api/hosts/setEnable/{id}","method":"post"},{"path":"/panel/api/hosts/reorder","method":"post"},{"path":"/panel/api/hosts/bulk/setEnable","method":"post"},{"path":"/panel/api/hosts/bulk/del","method":"post"}]} showTitle />
|
||||
<Comp document="./public/openapi.json" webhooks={[]} operations={[{"path":"/panel/api/hosts/list","method":"get"},{"path":"/panel/api/hosts/get/{groupId}","method":"get"},{"path":"/panel/api/hosts/byInbound/{inboundId}","method":"get"},{"path":"/panel/api/hosts/tags","method":"get"},{"path":"/panel/api/hosts/add","method":"post"},{"path":"/panel/api/hosts/update/{groupId}","method":"post"},{"path":"/panel/api/hosts/del/{groupId}","method":"post"},{"path":"/panel/api/hosts/setEnable/{groupId}","method":"post"},{"path":"/panel/api/hosts/reorder","method":"post"},{"path":"/panel/api/hosts/bulk/setEnable","method":"post"},{"path":"/panel/api/hosts/bulk/del","method":"post"}]} showTitle />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,11 +1,12 @@
|
||||
---
|
||||
title: 出站与路由
|
||||
description: 在 3x-ui 中调整出口流量——WARP 与 NordVPN 出站、出站订阅(服务器池)、路由规则以及负载均衡器。
|
||||
description: 在 3x-ui 中调整出口流量——WARP、NordVPN、PIA WireGuard、出站订阅(服务器池)、路由规则以及负载均衡器。
|
||||
icon: Route
|
||||
---
|
||||
|
||||
入站负责接受客户端;**出站**则决定客户端的流量接下来发往何处。
|
||||
3x-ui 可以让流量经由 Cloudflare WARP、NordVPN,或从订阅导入的任意出站池转发,
|
||||
3x-ui 可以让流量经由 Cloudflare WARP、NordVPN、Private Internet Access
|
||||
(WireGuard),或从订阅导入的任意出站池转发,
|
||||
并通过路由规则和均衡器在它们之间进行选择。
|
||||
|
||||
## 编辑出站与路由
|
||||
@@ -81,6 +82,19 @@ WARP 账户,并将其接入一个标签为 **`warp`** 的 WireGuard 出站:
|
||||
直接接受一个私钥),并列出国家/服务器,从而让你构建一个
|
||||
NordVPN 出站。
|
||||
|
||||
## PIA WireGuard
|
||||
|
||||
3x-ui 可以用 PIA 用户名和密码登录,从已验签的服务器列表里选择国家/区域/服务器,
|
||||
并生成 WireGuard 出站。打开 **Xray → 出站 → 更多 → PIA**,登录后选服务器并添加出站。
|
||||
可以添加多台服务器(每个 hostname 一条出站)。标签为 `pia-<region>-<server>`(例如
|
||||
`pia-us-east-useast1`)。添加或对该行 **Reset** 会向该服务器的 PIA `/addKey` 注册密钥。
|
||||
同一 hostname 不能添加两次。登出只清除保存的 token;删除出站请在出站列表里操作。
|
||||
Reset 或删除出站不会撤销 PIA 账户侧的 WireGuard peer。
|
||||
|
||||
密码不落库。PIA API token 与节点 API token 共用 `NODE_TOKEN_ENCRYPTION`。
|
||||
若在未重新登录 PIA 的情况下淘汰旧的 `XUI_NODE_TOKEN_KEY`,Add/Reset 会失败,直到再次登录。
|
||||
对端 `allowedIPs` 仅为 `0.0.0.0/0`(IPv4)。
|
||||
|
||||
## 出站订阅(服务器池)
|
||||
|
||||
**出站订阅**会导入一个远程分享链接订阅,并将其中的服务器作为**出站**注入到正在运行的
|
||||
|
||||
@@ -30,11 +30,9 @@ _openapi:
|
||||
#fetch-one-client-by-email-including-the-inbound-ids-and-external-config-ids-it-is-attached-to
|
||||
- depth: 2
|
||||
title: >-
|
||||
在一次调用中创建一个新客户端并将其挂载到一个或多个入站。请求体为 JSON。各协议的密钥
|
||||
(VLESS/VMess 的 UUID、Trojan/Shadowsocks 的 password、Hysteria 的 auth)在
|
||||
省略时由服务端生成,因此调用方只需发送通用字段。
|
||||
在一次调用中创建一个新客户端并将其挂载到一个或多个入站。请求体为 JSON。各协议的密钥在省略时由服务端生成,因此调用方只需发送通用字段。
|
||||
url: >-
|
||||
#create-a-new-client-and-attach-it-to-one-or-more-inbounds-in-a-single-call-body-is-json-per-protocol-secrets-uuid-for-vlessvmess-password-for-trojanshadowsocks-auth-for-hysteria-are-generated-server-side-when-omitted-so-callers-can-send-only-the-universal-fields
|
||||
#create-a-new-client-and-attach-it-to-one-or-more-inbounds-in-a-single-call-body-is-json-per-protocol-secrets-are-generated-server-side-when-omitted-so-callers-can-send-only-the-universal-fields
|
||||
- depth: 2
|
||||
title: >-
|
||||
按 email 更新现有客户端。变更会传播到每个挂载的入站。请求体为 JSON 客户端载荷——
|
||||
@@ -290,11 +288,9 @@ _openapi:
|
||||
id: >-
|
||||
fetch-one-client-by-email-including-the-inbound-ids-and-external-config-ids-it-is-attached-to
|
||||
- content: >-
|
||||
在一次调用中创建一个新客户端并将其挂载到一个或多个入站。请求体为 JSON。各协议的密钥
|
||||
(VLESS/VMess 的 UUID、Trojan/Shadowsocks 的 password、Hysteria 的 auth)在
|
||||
省略时由服务端生成,因此调用方只需发送通用字段。
|
||||
在一次调用中创建一个新客户端并将其挂载到一个或多个入站。请求体为 JSON。各协议的密钥在省略时由服务端生成,因此调用方只需发送通用字段。
|
||||
id: >-
|
||||
create-a-new-client-and-attach-it-to-one-or-more-inbounds-in-a-single-call-body-is-json-per-protocol-secrets-uuid-for-vlessvmess-password-for-trojanshadowsocks-auth-for-hysteria-are-generated-server-side-when-omitted-so-callers-can-send-only-the-universal-fields
|
||||
create-a-new-client-and-attach-it-to-one-or-more-inbounds-in-a-single-call-body-is-json-per-protocol-secrets-are-generated-server-side-when-omitted-so-callers-can-send-only-the-universal-fields
|
||||
- content: >-
|
||||
按 email 更新现有客户端。变更会传播到每个挂载的入站。请求体为 JSON 客户端载荷——
|
||||
请提供你希望保留的完整字段集(服务端会替换整条记录,而非局部更新)。
|
||||
@@ -493,7 +489,32 @@ _openapi:
|
||||
(socks、http、mixed、wireguard、dokodemo、tunnel)不产生任何内容。
|
||||
id: >-
|
||||
return-every-url-for-one-client-across-all-attached-inbounds--the-same-strings-the-copy-url-button-copies-in-the-panel-ui-supported-protocols-vmess-vless-trojan-shadowsocks-hysteria-if-streamsettingsexternalproxy-is-set-returns-one-url-per-external-proxy-protocols-without-a-url-form-socks-http-mixed-wireguard-dokodemo-tunnel-contribute-nothing
|
||||
contents: []
|
||||
contents:
|
||||
- content: >-
|
||||
服务端在字段被省略时自动填充;调用方提供的有效值不会被覆盖。若以已存在的 email 重新添加,且其已存储的 `subId` 一致,则沿用已存储的 `id`、`password`、`auth` 和 `secret`,而不是重新生成,以保证同一身份在其各个入站之间保持一致。
|
||||
|
||||
|
||||
- **VLESS / VMess** —— `id`,新生成的 UUID
|
||||
|
||||
- **Trojan** —— `password`
|
||||
|
||||
- **Shadowsocks** —— `password`。在 `2022-blake3-*` 入站上,若调用方提供的 password 经 base64 解码后的长度不等于该加密方式所需的密钥长度(16 或 32 字节),它会被替换为服务端生成的密钥,且调用仍然返回成功;因此若不打算交由服务端生成,请回读该客户端确认。传统加密方式则保留任何非空 password
|
||||
|
||||
- **Hysteria** —— `auth`
|
||||
|
||||
- **mtproto** —— `secret`,由该入站的伪装域名派生的 FakeTLS 密钥;该入站未设置伪装域名时,则取自 `www.cloudflare.com`
|
||||
|
||||
- **WireGuard** —— 两个密钥都为空时生成 `privateKey` 与 `publicKey`;只提供了 `privateKey` 时仅推导 `publicKey`。此外还会分配 `allowedIPs`:从该入站现有对端所在的 /24 中取一个空闲的 `/32`,若该入站尚无对端,则取自 `10.0.0.0/24`
|
||||
|
||||
|
||||
同一请求体也接受、但服务端不会自动生成的字段:`preSharedKey` 与 `keepAlive`(WireGuard)、`adTag`(mtproto)。
|
||||
|
||||
|
||||
其中只有 WireGuard 这一步可能失败。分配地址时会先把搜索范围扩大到所属的 /16,之后才以 `wireguard: no free address available in <scope>` 放弃;而调用方自行提供的 `allowedIPs` 只做校验、不做分配:当同一入站上的另一个客户端已占用该地址时,返回 `wireguard: allowedIPs entry already used by another client: <address>`。该校验按入站进行,因此同一地址出现在两个不同入站上是允许的。POST /panel/api/clients/{email}/attach 也执行同样的校验——已带有地址的客户端会把该地址带入新的入站。
|
||||
heading: create-a-new-client-and-attach-it-to-one-or-more-inbounds-in-a-single-call-body-is-json-per-protocol-secrets-are-generated-server-side-when-omitted-so-callers-can-send-only-the-universal-fields
|
||||
- content: >-
|
||||
WireGuard 客户端会把已存储的 `allowedIPs` 带入新入站,而不是获得新分配的地址;因此当目标入站上的另一个客户端已占用该地址时,调用会以 `wireguard: allowedIPs entry already used by another client: <address>` 失败。请先在该入站上释放该地址——完整规则见 POST /panel/api/clients/add。
|
||||
heading: attach-an-existing-client-to-one-or-more-additional-inbounds-body-is-json
|
||||
---
|
||||
|
||||
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
|
||||
|
||||
@@ -102,7 +102,7 @@ export default function Layout(props) {
|
||||
return (
|
||||
<>
|
||||
{props.children}
|
||||
<Comp document="./public/openapi.json" webhooks={[]} operations={[{"path":"/panel/api/hosts/list","method":"get"},{"path":"/panel/api/hosts/get/{id}","method":"get"},{"path":"/panel/api/hosts/byInbound/{inboundId}","method":"get"},{"path":"/panel/api/hosts/tags","method":"get"},{"path":"/panel/api/hosts/add","method":"post"},{"path":"/panel/api/hosts/update/{id}","method":"post"},{"path":"/panel/api/hosts/del/{id}","method":"post"},{"path":"/panel/api/hosts/setEnable/{id}","method":"post"},{"path":"/panel/api/hosts/reorder","method":"post"},{"path":"/panel/api/hosts/bulk/setEnable","method":"post"},{"path":"/panel/api/hosts/bulk/del","method":"post"}]} showTitle />
|
||||
<Comp document="./public/openapi.json" webhooks={[]} operations={[{"path":"/panel/api/hosts/list","method":"get"},{"path":"/panel/api/hosts/get/{groupId}","method":"get"},{"path":"/panel/api/hosts/byInbound/{inboundId}","method":"get"},{"path":"/panel/api/hosts/tags","method":"get"},{"path":"/panel/api/hosts/add","method":"post"},{"path":"/panel/api/hosts/update/{groupId}","method":"post"},{"path":"/panel/api/hosts/del/{groupId}","method":"post"},{"path":"/panel/api/hosts/setEnable/{groupId}","method":"post"},{"path":"/panel/api/hosts/reorder","method":"post"},{"path":"/panel/api/hosts/bulk/setEnable","method":"post"},{"path":"/panel/api/hosts/bulk/del","method":"post"}]} showTitle />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -22,28 +22,28 @@ The panel uses standard Go `html/template` to render the subscription page.
|
||||
|
||||
When rendering the template, the following variables are injected into the template context (`{{ .variable }}`):
|
||||
|
||||
* `{{ .sId }}`: Subscription ID (UUID).
|
||||
* `{{ .enabled }}`: Whether the subscription/client is enabled (boolean).
|
||||
* `{{ .isOnline }}`: Whether the subscription's client has a live connection right now (boolean). Computed from the panel's online-client tracking (local Xray plus any remote nodes) at render time.
|
||||
* `{{ .download }}`: Formatted download traffic (e.g. "2.5 GB").
|
||||
* `{{ .upload }}`: Formatted upload traffic.
|
||||
* `{{ .total }}`: Formatted total traffic limit.
|
||||
* `{{ .used }}`: Formatted used traffic (download + upload).
|
||||
* `{{ .remained }}`: Formatted remaining traffic.
|
||||
* `{{ .expire }}`: Expiration time as an int64 Unix timestamp in **seconds** (`0` means never). Multiply by 1000 for a JavaScript `Date`.
|
||||
* `{{ .lastOnline }}`: Last online time as an int64 Unix timestamp in **milliseconds** (`0` means never seen).
|
||||
* `{{ .downloadByte }}`: Download traffic in exact bytes (int64).
|
||||
* `{{ .uploadByte }}`: Upload traffic in exact bytes (int64).
|
||||
* `{{ .totalByte }}`: Total traffic limit in exact bytes (int64).
|
||||
* `{{ .subUrl }}`: The URL of the subscription page.
|
||||
* `{{ .subJsonUrl }}`: The URL for the JSON configuration of the subscription.
|
||||
* `{{ .subClashUrl }}`: The URL for the Clash/Mihomo configuration.
|
||||
* `{{ .subTitle }}`: The subscription title configured in the panel (Subscription → Information). Useful for page branding/headings. May be empty.
|
||||
* `{{ .subSupportUrl }}`: The support URL configured in the panel. Useful for a "Contact support" link. May be empty.
|
||||
* `{{ .links }}`: A list (slice) of string configurations (VMess, VLESS, etc. URLs). You can loop through them using `{{ range .links }} ... {{ end }}`.
|
||||
* `{{ .emails }}`: A list (slice) of client emails, parallel to `links` — the email at index *i* owns the link at index *i*. May contain duplicates when one client has several links.
|
||||
* `{{ .announce }}`: The announcement text configured in the panel (Settings → Subscription → Announce). May be empty.
|
||||
* `{{ .datepicker }}`: Current calendar format used by the panel (e.g. "gregorian" or "jalali").
|
||||
- `{{ .sId }}`: Subscription ID (UUID).
|
||||
- `{{ .enabled }}`: Whether the subscription/client is enabled (boolean).
|
||||
- `{{ .isOnline }}`: Whether the subscription's client has a live connection right now (boolean). Computed from the panel's online-client tracking (local Xray plus any remote nodes) at render time.
|
||||
- `{{ .download }}`: Formatted download traffic (e.g. "2.5 GB").
|
||||
- `{{ .upload }}`: Formatted upload traffic.
|
||||
- `{{ .total }}`: Formatted total traffic limit.
|
||||
- `{{ .used }}`: Formatted used traffic (download + upload).
|
||||
- `{{ .remained }}`: Formatted remaining traffic.
|
||||
- `{{ .expire }}`: Expiration time as an int64 Unix timestamp in **seconds** (`0` means never). Multiply by 1000 for a JavaScript `Date`.
|
||||
- `{{ .lastOnline }}`: Last online time as an int64 Unix timestamp in **milliseconds** (`0` means never seen).
|
||||
- `{{ .downloadByte }}`: Download traffic in exact bytes (int64).
|
||||
- `{{ .uploadByte }}`: Upload traffic in exact bytes (int64).
|
||||
- `{{ .totalByte }}`: Total traffic limit in exact bytes (int64).
|
||||
- `{{ .subUrl }}`: The URL of the subscription page.
|
||||
- `{{ .subJsonUrl }}`: The URL for the JSON configuration of the subscription.
|
||||
- `{{ .subClashUrl }}`: The URL for the Clash/Mihomo configuration.
|
||||
- `{{ .subTitle }}`: The subscription title configured in the panel (Subscription → Information). Useful for page branding/headings. May be empty.
|
||||
- `{{ .subSupportUrl }}`: The support URL configured in the panel. Useful for a "Contact support" link. May be empty.
|
||||
- `{{ .links }}`: A list (slice) of string configurations (VMess, VLESS, etc. URLs). You can loop through them using `{{ range .links }} ... {{ end }}`.
|
||||
- `{{ .emails }}`: A list (slice) of client emails, parallel to `links` — the email at index _i_ owns the link at index _i_. May contain duplicates when one client has several links.
|
||||
- `{{ .announce }}`: The announcement text configured in the panel (Settings → Subscription → Announce). May be empty.
|
||||
- `{{ .datepicker }}`: Current calendar format used by the panel (e.g. "gregorian" or "jalali").
|
||||
|
||||
## Live Status JSON (`?format=info`)
|
||||
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
import coreWebVitals from 'eslint-config-next/core-web-vitals';
|
||||
import typescript from 'eslint-config-next/typescript';
|
||||
|
||||
/** @type {import('eslint').Linter.Config[]} */
|
||||
const config = [
|
||||
{
|
||||
ignores: [
|
||||
'.next/**',
|
||||
'.source/**',
|
||||
'out/**',
|
||||
'node_modules/**',
|
||||
'next-env.d.ts',
|
||||
// Generated API reference pages (fumadocs-openapi output)
|
||||
'content/docs/**/reference/api/**',
|
||||
],
|
||||
},
|
||||
...coreWebVitals,
|
||||
...typescript,
|
||||
];
|
||||
|
||||
export default config;
|
||||
@@ -3,7 +3,14 @@ import { Heart } from 'lucide-react';
|
||||
import { Logo } from '@/components/logo';
|
||||
import { TelegramIcon } from '@/components/icons';
|
||||
import { DocsThemeSwitch } from '@/components/theme-switch';
|
||||
import { appName, productRepoUrl, telegramChannel, telegramChannelUrl, donateUrl, siteUrl } from './shared';
|
||||
import {
|
||||
appName,
|
||||
productRepoUrl,
|
||||
telegramChannel,
|
||||
telegramChannelUrl,
|
||||
donateUrl,
|
||||
siteUrl,
|
||||
} from './shared';
|
||||
import { getSiteMessages } from './site-i18n';
|
||||
|
||||
// Build locale-aware shared layout options. With `hideLocale: 'default-locale'`,
|
||||
|
||||
@@ -222,7 +222,8 @@ const zh: SiteMessages = {
|
||||
},
|
||||
{
|
||||
title: '自托管且可脚本化',
|
||||
description: '单个 Go 二进制文件或 Docker 镜像、SQLite/PostgreSQL 后端,以及用于自动化的完整 REST API。',
|
||||
description:
|
||||
'单个 Go 二进制文件或 Docker 镜像、SQLite/PostgreSQL 后端,以及用于自动化的完整 REST API。',
|
||||
},
|
||||
],
|
||||
licenseBefore: '基于 ',
|
||||
|
||||
@@ -31,7 +31,7 @@ const base = {
|
||||
describe('buildCurl', () => {
|
||||
it('GET emits the Bearer header, a single-quoted URL, and no body flag', () => {
|
||||
const cmd = buildCurl({ ...base, method: 'GET' });
|
||||
expect(cmd).toContain("-X GET");
|
||||
expect(cmd).toContain('-X GET');
|
||||
expect(cmd).toContain("-H 'Authorization: Bearer TKN'");
|
||||
expect(cmd).toContain("'https://panel.example.com:2053/panel/api/inbounds/list'");
|
||||
expect(cmd).not.toContain('--data');
|
||||
@@ -39,14 +39,23 @@ describe('buildCurl', () => {
|
||||
});
|
||||
|
||||
it('POST with a body emits --data and a JSON content type', () => {
|
||||
const cmd = buildCurl({ ...base, method: 'POST', path: '/panel/api/inbounds/add', body: '{"up":0}' });
|
||||
const cmd = buildCurl({
|
||||
...base,
|
||||
method: 'POST',
|
||||
path: '/panel/api/inbounds/add',
|
||||
body: '{"up":0}',
|
||||
});
|
||||
expect(cmd).toContain('-X POST');
|
||||
expect(cmd).toContain("--data '{\"up\":0}'");
|
||||
expect(cmd).toContain("Content-Type: application/json");
|
||||
expect(cmd).toContain('--data \'{"up":0}\'');
|
||||
expect(cmd).toContain('Content-Type: application/json');
|
||||
});
|
||||
|
||||
it('POST without a body omits --data', () => {
|
||||
const cmd = buildCurl({ ...base, method: 'POST', path: '/panel/api/inbounds/resetAllTraffics' });
|
||||
const cmd = buildCurl({
|
||||
...base,
|
||||
method: 'POST',
|
||||
path: '/panel/api/inbounds/resetAllTraffics',
|
||||
});
|
||||
expect(cmd).not.toContain('--data');
|
||||
});
|
||||
});
|
||||
@@ -60,7 +69,12 @@ describe('buildFetchSnippet', () => {
|
||||
});
|
||||
|
||||
it('POST with a body includes a JSON.stringify body', () => {
|
||||
const snip = buildFetchSnippet({ ...base, method: 'POST', path: '/panel/api/inbounds/add', body: '{"up":0}' });
|
||||
const snip = buildFetchSnippet({
|
||||
...base,
|
||||
method: 'POST',
|
||||
path: '/panel/api/inbounds/add',
|
||||
body: '{"up":0}',
|
||||
});
|
||||
expect(snip).toContain("method: 'POST'");
|
||||
expect(snip).toContain('body: JSON.stringify(');
|
||||
});
|
||||
|
||||
@@ -160,7 +160,12 @@ describe('buildOutbound — wireguard & warp', () => {
|
||||
const ob = buildOutbound({
|
||||
kind: 'wireguard',
|
||||
tag: 'wg',
|
||||
wireguard: { secretKey: 'sk', address: ['10.0.0.2/32'], publicKey: 'pk', endpoint: 'host:51820' },
|
||||
wireguard: {
|
||||
secretKey: 'sk',
|
||||
address: ['10.0.0.2/32'],
|
||||
publicKey: 'pk',
|
||||
endpoint: 'host:51820',
|
||||
},
|
||||
});
|
||||
const s = ob.settings as Record<string, unknown>;
|
||||
expect(s.secretKey).toBe('sk');
|
||||
|
||||
@@ -162,7 +162,11 @@ function buildSettings(o: OutboundInput): Record<string, unknown> {
|
||||
],
|
||||
};
|
||||
case 'trojan':
|
||||
return { servers: [{ address: s?.address ?? '', port: toPort(s?.port), password: s?.password ?? '' }] };
|
||||
return {
|
||||
servers: [
|
||||
{ address: s?.address ?? '', port: toPort(s?.port), password: s?.password ?? '' },
|
||||
],
|
||||
};
|
||||
case 'shadowsocks':
|
||||
return {
|
||||
servers: [
|
||||
|
||||
@@ -18,7 +18,12 @@ describe('buildBalancer', () => {
|
||||
});
|
||||
|
||||
it('includes fallbackTag when set', () => {
|
||||
const b = buildBalancer({ tag: 'lb', selector: ['a'], strategy: 'random', fallbackTag: 'direct' });
|
||||
const b = buildBalancer({
|
||||
tag: 'lb',
|
||||
selector: ['a'],
|
||||
strategy: 'random',
|
||||
fallbackTag: 'direct',
|
||||
});
|
||||
expect(b.fallbackTag).toBe('direct');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -121,7 +121,10 @@ export function buildRouting(input: RoutingInput): Record<string, unknown> {
|
||||
if (input.observatory) {
|
||||
Object.assign(out, buildObservatory(input.observatory));
|
||||
} else if (input.balancers.some((b) => b.strategy === 'leastLoad')) {
|
||||
Object.assign(out, buildObservatory({ mode: 'burst', subjectSelector: uniqueSelectors(input.balancers) }));
|
||||
Object.assign(
|
||||
out,
|
||||
buildObservatory({ mode: 'burst', subjectSelector: uniqueSelectors(input.balancers) }),
|
||||
);
|
||||
} else if (input.balancers.some((b) => b.strategy === 'leastPing')) {
|
||||
Object.assign(
|
||||
out,
|
||||
|
||||
@@ -214,12 +214,20 @@ function proxyOutbound(c: SubClient): Record<string, unknown> {
|
||||
};
|
||||
break;
|
||||
case 'trojan':
|
||||
settings = { servers: [{ address: c.address, port: c.port, password: c.password ?? '', level: 8 }] };
|
||||
settings = {
|
||||
servers: [{ address: c.address, port: c.port, password: c.password ?? '', level: 8 }],
|
||||
};
|
||||
break;
|
||||
case 'ss':
|
||||
settings = {
|
||||
servers: [
|
||||
{ address: c.address, port: c.port, password: c.password ?? '', level: 8, method: c.method || '' },
|
||||
{
|
||||
address: c.address,
|
||||
port: c.port,
|
||||
password: c.password ?? '',
|
||||
level: 8,
|
||||
method: c.method || '',
|
||||
},
|
||||
],
|
||||
};
|
||||
break;
|
||||
@@ -233,6 +241,8 @@ function proxyOutbound(c: SubClient): Record<string, unknown> {
|
||||
};
|
||||
}
|
||||
|
||||
// Mirrors the one-document-per-client model only; the panel also emits
|
||||
// balancer documents (sub_balancers) that are intentionally out of scope here.
|
||||
function jsonConfig(c: SubClient): Record<string, unknown> {
|
||||
return {
|
||||
remarks: c.remark,
|
||||
|
||||
@@ -36,7 +36,10 @@ describe('parseAdminIds', () => {
|
||||
});
|
||||
|
||||
it('accepts negative group ids and captures invalid entries', () => {
|
||||
expect(parseAdminIds('-1001234567, abc, 42')).toEqual({ ids: [-1001234567, 42], invalid: ['abc'] });
|
||||
expect(parseAdminIds('-1001234567, abc, 42')).toEqual({
|
||||
ids: [-1001234567, 42],
|
||||
invalid: ['abc'],
|
||||
});
|
||||
});
|
||||
|
||||
it('returns empty for blank input', () => {
|
||||
@@ -78,9 +81,9 @@ describe('telegramApiBase', () => {
|
||||
|
||||
describe('renderMessageTemplate', () => {
|
||||
it('substitutes known variables', () => {
|
||||
expect(renderMessageTemplate('Host {{host}} up {{uptime}}', { host: 'srv', uptime: '3d' })).toBe(
|
||||
'Host srv up 3d',
|
||||
);
|
||||
expect(
|
||||
renderMessageTemplate('Host {{host}} up {{uptime}}', { host: 'srv', uptime: '3d' }),
|
||||
).toBe('Host srv up 3d');
|
||||
});
|
||||
|
||||
it('leaves unknown variables literal', () => {
|
||||
@@ -90,7 +93,11 @@ describe('renderMessageTemplate', () => {
|
||||
|
||||
describe('buildBotConfigSummary', () => {
|
||||
it('emits the panel settings keys with admin ids joined', () => {
|
||||
const s = buildBotConfigSummary({ token: VALID_TOKEN, adminIds: '111, 222', runTime: '@daily' });
|
||||
const s = buildBotConfigSummary({
|
||||
token: VALID_TOKEN,
|
||||
adminIds: '111, 222',
|
||||
runTime: '@daily',
|
||||
});
|
||||
expect(s.tgBotEnable).toBe(true);
|
||||
expect(s.tgBotToken).toBe(VALID_TOKEN);
|
||||
expect(s.tgBotChatId).toBe('111,222');
|
||||
|
||||
@@ -43,7 +43,10 @@ export function validateBotToken(token: string): TokenValidation {
|
||||
export function parseAdminIds(raw: string): AdminIdsResult {
|
||||
const ids: number[] = [];
|
||||
const invalid: string[] = [];
|
||||
for (const part of raw.split(',').map((s) => s.trim()).filter(Boolean)) {
|
||||
for (const part of raw
|
||||
.split(',')
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean)) {
|
||||
// Telegram chat ids are integers; group/channel ids are negative.
|
||||
if (/^-?\d+$/.test(part)) ids.push(Number(part));
|
||||
else invalid.push(part);
|
||||
|
||||
+16
-19
@@ -11,28 +11,27 @@
|
||||
"postinstall": "fumadocs-mdx",
|
||||
"gen:api": "node scripts/gen-openapi.ts",
|
||||
"typecheck": "fumadocs-mdx && next typegen && tsc --noEmit",
|
||||
"lint": "eslint .",
|
||||
"format": "prettier --write .",
|
||||
"format:check": "prettier --check .",
|
||||
"lint": "oxlint .",
|
||||
"format": "oxfmt .",
|
||||
"format:check": "oxfmt --check .",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@orama/orama": "^3.1.18",
|
||||
"fumadocs-core": "^16.14.3",
|
||||
"fumadocs-core": "^16.14.5",
|
||||
"fumadocs-docgen": "^3.1.0",
|
||||
"fumadocs-mdx": "^15.2.3",
|
||||
"fumadocs-openapi": "^11.2.3",
|
||||
"fumadocs-ui": "^16.14.3",
|
||||
"lucide-react": "^1.31.0",
|
||||
"mermaid": "^11.16.1",
|
||||
"next": "16.3.0",
|
||||
"fumadocs-mdx": "^15.3.0",
|
||||
"fumadocs-openapi": "^11.2.4",
|
||||
"fumadocs-ui": "^16.14.5",
|
||||
"lucide-react": "^1.33.0",
|
||||
"mermaid": "^11.17.0",
|
||||
"next": "16.3.1",
|
||||
"next-themes": "^0.4.6",
|
||||
"react": "^19.2.8",
|
||||
"react-dom": "^19.2.8",
|
||||
"react-qr-code": "^2.2.0",
|
||||
"tailwind-merge": "^3.6.0",
|
||||
"zbsearch": "3.3.4",
|
||||
"zbsearch": "4.0.0",
|
||||
"zod": "^4.4.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -41,14 +40,12 @@
|
||||
"@types/node": "^26.2.0",
|
||||
"@types/react": "^19.2.18",
|
||||
"@types/react-dom": "^19.2.4",
|
||||
"eslint": "^9.39.5",
|
||||
"eslint-config-next": "16.3.0",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"oxfmt": "0.64.0",
|
||||
"oxlint": "1.79.0",
|
||||
"postcss": "^8.5.26",
|
||||
"prettier": "^3.9.6",
|
||||
"tailwindcss": "^4.3.3",
|
||||
"typescript": "6.0.3",
|
||||
"vitest": "^4.1.10"
|
||||
"typescript": "7.0.2",
|
||||
"vitest": "^4.1.11"
|
||||
},
|
||||
"packageManager": "pnpm@11.21.0+sha512.521705bce689924eac72f5a3587122f362689ef6571e55ba80076fd637c11132ecffada26fad4ea79c485bfddbfd3d5a2a5b05805a77e893de71ec8a6cca3bb1"
|
||||
"packageManager": "pnpm@11.22.0+sha512.1ff870c4c6133dfd88fb2afc46dd13d47f09c9794b438c6fdb47ca98caf3bc16381ee0be93a091b8e3824cf01f889f46d7d9e20910fb0be1ab0fb5baa80dd621"
|
||||
}
|
||||
|
||||
Generated
+941
-3109
File diff suppressed because it is too large
Load Diff
@@ -8,9 +8,8 @@ overrides:
|
||||
'postcss@<8.5.10': '^8.5.15'
|
||||
'sharp@<0.35.0': '^0.35.3'
|
||||
minimumReleaseAgeExclude:
|
||||
- '@mermaid-js/parser@1.2.0'
|
||||
- mermaid@11.16.0
|
||||
- fumadocs-core@16.14.1
|
||||
- fumadocs-ui@16.14.1
|
||||
- lucide-react@1.29.0
|
||||
- '@mermaid-js/parser@1.2.1'
|
||||
- mermaid@11.17.0
|
||||
- lucide-react@1.33.0
|
||||
- postcss@8.5.26
|
||||
- fumadocs-mdx@15.3.0
|
||||
|
||||
+3165
-462
File diff suppressed because it is too large
Load Diff
+10
-10
@@ -14,11 +14,11 @@ list, and multi-node sync — so once it is set, everything downstream just work
|
||||
Open an inbound → **Transport / Stream Settings** → enable **Sockopt** → use the
|
||||
**Real client IP** preset selector:
|
||||
|
||||
| Preset | What it does | Use for |
|
||||
|---|---|---|
|
||||
| **Off / direct** | Clears both fields. | Inbound reachable directly by clients. |
|
||||
| **Cloudflare CDN** | Sets `sockopt.trustedXForwardedFor = ["CF-Connecting-IP"]`. | WebSocket / HTTPUpgrade / XHTTP behind Cloudflare's CDN (orange cloud). |
|
||||
| **L4 relay / Spectrum (PROXY)** | Sets `acceptProxyProtocol = true`. | An L4 tunnel/relay in front, or Cloudflare **Spectrum**. |
|
||||
| Preset | What it does | Use for |
|
||||
| ------------------------------- | ----------------------------------------------------------- | ----------------------------------------------------------------------- |
|
||||
| **Off / direct** | Clears both fields. | Inbound reachable directly by clients. |
|
||||
| **Cloudflare CDN** | Sets `sockopt.trustedXForwardedFor = ["CF-Connecting-IP"]`. | WebSocket / HTTPUpgrade / XHTTP behind Cloudflare's CDN (orange cloud). |
|
||||
| **L4 relay / Spectrum (PROXY)** | Sets `acceptProxyProtocol = true`. | An L4 tunnel/relay in front, or Cloudflare **Spectrum**. |
|
||||
|
||||
The raw `Proxy Protocol` switch and `Trusted X-Forwarded-For` list stay visible below the preset
|
||||
selector for manual / advanced tuning — the presets just fill them in for you.
|
||||
@@ -65,16 +65,16 @@ and XHTTP; **not** on mKCP. The front must be configured to send the header, e.g
|
||||
|
||||
## Transport support matrix
|
||||
|
||||
| Mechanism | TCP/RAW | mKCP | WebSocket | gRPC | HTTPUpgrade | XHTTP |
|
||||
|---|:--:|:--:|:--:|:--:|:--:|:--:|
|
||||
| `trustedXForwardedFor` (header) | – | – | ✅ | – | ✅ | ✅ |
|
||||
| `acceptProxyProtocol` (PROXY) | ✅ | – | ✅ | ✅ | ✅ | ✅ |
|
||||
| Mechanism | TCP/RAW | mKCP | WebSocket | gRPC | HTTPUpgrade | XHTTP |
|
||||
| ------------------------------- | :-----: | :--: | :-------: | :--: | :---------: | :---: |
|
||||
| `trustedXForwardedFor` (header) | – | – | ✅ | – | ✅ | ✅ |
|
||||
| `acceptProxyProtocol` (PROXY) | ✅ | – | ✅ | ✅ | ✅ | ✅ |
|
||||
|
||||
The form shows a warning when you select a preset that the current transport cannot honor.
|
||||
|
||||
> **Use one, not both.** `acceptProxyProtocol` and `trustedXForwardedFor` are independent — the
|
||||
> first reads the real IP from the L4 PROXY header, the second from an HTTP request header. On
|
||||
> WebSocket / HTTPUpgrade / XHTTP, xray applies the HTTP header *last*, so a stale
|
||||
> WebSocket / HTTPUpgrade / XHTTP, xray applies the HTTP header _last_, so a stale
|
||||
> `trustedXForwardedFor` would override (and defeat) a PROXY-protocol setup. The presets are
|
||||
> mutually exclusive and clear the other field for you; only mix them by hand if you know your
|
||||
> upstream chain needs it.
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"$schema": "./node_modules/oxfmt/configuration_schema.json",
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all",
|
||||
"printWidth": 100,
|
||||
"tabWidth": 2,
|
||||
"ignorePatterns": [
|
||||
"node_modules",
|
||||
"src/generated",
|
||||
"public",
|
||||
"tools/oxlint/__fixtures__"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
{
|
||||
"$schema": "./node_modules/oxlint/configuration_schema.json",
|
||||
"ignorePatterns": [
|
||||
"node_modules/**"
|
||||
],
|
||||
"plugins": [
|
||||
"typescript",
|
||||
"react",
|
||||
"jsx-a11y"
|
||||
],
|
||||
"jsPlugins": [
|
||||
"./tools/oxlint/input-number-guard.mjs"
|
||||
],
|
||||
"categories": {
|
||||
"correctness": "error"
|
||||
},
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es2022": true
|
||||
},
|
||||
"rules": {
|
||||
"typescript/no-explicit-any": "error",
|
||||
"typescript/no-unused-vars": [
|
||||
"warn",
|
||||
{
|
||||
"argsIgnorePattern": "^_",
|
||||
"varsIgnorePattern": "^_",
|
||||
"caughtErrorsIgnorePattern": "^_"
|
||||
}
|
||||
],
|
||||
"typescript/ban-ts-comment": "error",
|
||||
"typescript/no-empty-object-type": "error",
|
||||
"typescript/no-namespace": "error",
|
||||
"typescript/no-require-imports": "error",
|
||||
"typescript/no-this-alias": "error",
|
||||
"typescript/no-unsafe-function-type": "error",
|
||||
"typescript/no-unused-expressions": "warn",
|
||||
"typescript/no-wrapper-object-types": "error",
|
||||
"typescript/prefer-as-const": "error",
|
||||
"typescript/triple-slash-reference": "error",
|
||||
"no-empty": [
|
||||
"error",
|
||||
{
|
||||
"allowEmptyCatch": true
|
||||
}
|
||||
],
|
||||
"react-hooks/rules-of-hooks": "error",
|
||||
"react-hooks/exhaustive-deps": "error",
|
||||
"jsx-a11y/no-autofocus": "off",
|
||||
"input-number/no-synthetic-clear": "off",
|
||||
"jsx-a11y/prefer-tag-over-role": "off"
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": [
|
||||
"src/pages/settings/**/*.tsx",
|
||||
"src/pages/xray/**/*.tsx"
|
||||
],
|
||||
"rules": {
|
||||
"input-number/no-synthetic-clear": "error"
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": [
|
||||
"src/pages/xray/**/*Modal.tsx"
|
||||
],
|
||||
"rules": {
|
||||
"input-number/no-synthetic-clear": "off"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
+1
-1
@@ -33,7 +33,7 @@ The `@` import alias maps to `src/`.
|
||||
- Function components + hooks only; no class components.
|
||||
- Comments in committed TS/TSX: 2 lines MAX per comment block, spent on the
|
||||
*why* a name cannot hold (same rule as root CLAUDE.md). HTML comments are fine.
|
||||
- TS strict; `no-explicit-any` is an error. Build forms with `useZodForm` +
|
||||
- TS strict; oxlint's `typescript/no-explicit-any` is an error. Build forms with `useZodForm` +
|
||||
`FormField` from `@/components/form/rhf` (wrap the tree in `FormProvider`);
|
||||
validate through the `zodResolver` or per-field
|
||||
`rules={{ validate: rhfZodValidate(Schema.shape.field) }}` — messages are Zod
|
||||
|
||||
+18
-11
@@ -33,7 +33,10 @@ production-style links work without round-tripping through Go.
|
||||
| `npm run build` | Regenerates OpenAPI + Zod, then builds into `../internal/web/dist/` |
|
||||
| `npm run preview` | Serve the built bundle locally |
|
||||
| `npm run typecheck` | `tsc --noEmit` (strict, no emit) |
|
||||
| `npm run lint` | ESLint flat config (`@typescript-eslint` + `react-hooks`) |
|
||||
| `npm run lint` | oxlint over `src/` + `tools/` (`.oxlintrc.json`) |
|
||||
| `npm run lint:deprecated` | Type-aware sweep for JSDoc `@deprecated` APIs (on demand) |
|
||||
| `npm run format` | oxfmt (`.oxfmtrc.json`) — rewrites `src/` + `tools/` in place |
|
||||
| `npm run format:check` | oxfmt in check mode (no writes) |
|
||||
| `npm run test` | Vitest single run (schema fixtures, link parsers, …) |
|
||||
| `npm run test:watch` | Vitest watch mode |
|
||||
| `npm run storybook` | Storybook dev server on `:6006` (component workbench + autodocs) |
|
||||
@@ -41,8 +44,8 @@ production-style links work without round-tripping through Go.
|
||||
| `npm run gen:api` | Build `public/openapi.json` from `pages/api-docs/endpoints.ts` |
|
||||
| `npm run gen:zod` | Run the Go-side openapigen tool → `src/generated/{zod,types}.ts` |
|
||||
|
||||
CI runs `typecheck`, `lint`, `test`, `build`, and `build-storybook` on
|
||||
every PR (see `../.github/workflows/ci.yml`).
|
||||
CI runs `typecheck`, `lint`, `format:check`, `test`, `build`, and
|
||||
`build-storybook` on every PR (see `../.github/workflows/ci.yml`).
|
||||
|
||||
### One-off: scan for deprecated APIs
|
||||
|
||||
@@ -51,12 +54,13 @@ with the JSDoc `@deprecated` tag (AntD prop renames, Zod renames,
|
||||
removed Web APIs, etc.):
|
||||
|
||||
```sh
|
||||
npx eslint --config eslint.deprecated.config.js src
|
||||
npm run lint:deprecated
|
||||
```
|
||||
|
||||
It's a type-aware ESLint run against `eslint.deprecated.config.js`
|
||||
and is not wired into `npm run lint` because typed linting triples
|
||||
the wall-clock time.
|
||||
It is oxlint's type-aware mode (`oxlint-tsgolint`, which drives the
|
||||
TypeScript 7 `typescript-go` checker) narrowed to `no-deprecated`, and
|
||||
is not wired into `npm run lint` because typed linting needs a full
|
||||
type-check pass.
|
||||
|
||||
## Production build
|
||||
|
||||
@@ -85,9 +89,12 @@ normal network requests.
|
||||
frontend/
|
||||
├── index.html, login.html, subpage.html # 3 Vite entries
|
||||
├── tsconfig.json
|
||||
├── eslint.config.js
|
||||
├── eslint.deprecated.config.js # On-demand type-aware lint config that flags
|
||||
│ # usages of APIs marked with JSDoc @deprecated
|
||||
├── .oxlintrc.json # oxlint config (replaces the ESLint flat config)
|
||||
├── .oxfmtrc.json # oxfmt config (Prettier-compatible settings)
|
||||
├── tools/oxlint/
|
||||
│ └── input-number-guard.mjs # oxlint JS plugin: the #6121/#6127 cleared-
|
||||
│ # InputNumber guard (oxlint has no
|
||||
│ # no-restricted-syntax)
|
||||
├── vitest.config.ts
|
||||
├── vite.config.js
|
||||
├── .storybook/ # Storybook config (main.ts, preview.tsx)
|
||||
@@ -155,7 +162,7 @@ Patterns:
|
||||
- Wire request: `Schema.parse(payload)` inside `mutationFn` — throws,
|
||||
because a malformed payload here is always a developer bug
|
||||
- **No `.loose()` or `[key: string]: any`** in production schemas.
|
||||
`@typescript-eslint/no-explicit-any: error` is enforced.
|
||||
`typescript/no-explicit-any: error` is enforced by oxlint.
|
||||
|
||||
## Form pattern (Pattern A)
|
||||
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
import js from '@eslint/js';
|
||||
import tseslint from 'typescript-eslint';
|
||||
import reactHooks from 'eslint-plugin-react-hooks';
|
||||
import jsxA11y from 'eslint-plugin-jsx-a11y';
|
||||
import globals from 'globals';
|
||||
|
||||
export default [
|
||||
{ ignores: ['node_modules/**', '../internal/web/dist/**'] },
|
||||
js.configs.recommended,
|
||||
...tseslint.configs.recommended.map((config) => ({
|
||||
...config,
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
})),
|
||||
{
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
plugins: {
|
||||
'react-hooks': reactHooks,
|
||||
},
|
||||
languageOptions: {
|
||||
ecmaVersion: 2022,
|
||||
sourceType: 'module',
|
||||
globals: {
|
||||
...globals.browser,
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
...reactHooks.configs.recommended.rules,
|
||||
'@typescript-eslint/no-unused-vars': ['warn', {
|
||||
argsIgnorePattern: '^_',
|
||||
varsIgnorePattern: '^_',
|
||||
caughtErrorsIgnorePattern: '^_',
|
||||
}],
|
||||
// Zod migration goal (Step 7): every production module is held to
|
||||
// strict no-explicit-any. The two legacy class files at the bottom
|
||||
// of the rule list keep their existing file-level eslint-disable
|
||||
// until DBInbound is migrated off Inbound.toInbound() — see the
|
||||
// migration spec Non-Goals section.
|
||||
'@typescript-eslint/no-explicit-any': 'error',
|
||||
'no-empty': ['error', { allowEmptyCatch: true }],
|
||||
'react-hooks/set-state-in-effect': 'off',
|
||||
'react-hooks/purity': 'off',
|
||||
'react-hooks/react-compiler': 'off',
|
||||
'react-hooks/preserve-manual-memoization': 'off',
|
||||
'react-hooks/immutability': 'off',
|
||||
'react-hooks/refs': 'off',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['**/*.tsx'],
|
||||
plugins: { 'jsx-a11y': jsxA11y },
|
||||
rules: {
|
||||
...jsxA11y.flatConfigs.recommended.rules,
|
||||
'jsx-a11y/no-autofocus': 'off',
|
||||
},
|
||||
},
|
||||
{
|
||||
// The settings and xray pages write numeric InputNumber changes straight
|
||||
// into state, so a null-collapsing handler (`Number(v) || N`, or the
|
||||
// ternary `typeof v === 'number' ? v : N`) turns a cleared field into a
|
||||
// stored N — the cleared-port bug, #6121. Handlers here go through
|
||||
// onNumber() (src/utils/onNumber.ts) instead. Known limit: a handler
|
||||
// extracted into a variable and passed as onChange={handler} is not
|
||||
// matched; the inline shapes below are the ones that drift in practice.
|
||||
files: ['src/pages/settings/**/*.tsx', 'src/pages/xray/**/*.tsx'],
|
||||
rules: {
|
||||
'no-restricted-syntax': ['error', {
|
||||
selector: 'JSXElement[openingElement.name.name="InputNumber"] JSXAttribute[name.name="onChange"] LogicalExpression[operator="||"] > CallExpression[callee.name="Number"]',
|
||||
message: 'A cleared InputNumber must not write a synthetic value; wrap the handler with onNumber() from @/utils/onNumber (see #6127).',
|
||||
}, {
|
||||
selector: 'JSXElement[openingElement.name.name="InputNumber"] JSXAttribute[name.name="onChange"] ConditionalExpression[test.left.operator="typeof"][alternate.type="Literal"]',
|
||||
message: 'A cleared InputNumber must not write a synthetic value; wrap the handler with onNumber() from @/utils/onNumber (see #6127).',
|
||||
}, {
|
||||
selector: 'JSXElement[openingElement.name.name="InputNumber"] JSXAttribute[name.name="onChange"] LogicalExpression[operator="??"][right.type="Literal"]',
|
||||
message: 'A cleared InputNumber must not write a synthetic value; wrap the handler with onNumber() from @/utils/onNumber (see #6127).',
|
||||
}],
|
||||
},
|
||||
},
|
||||
{
|
||||
// The xray form modals (OutboundFormModal, BalancerFormModal,
|
||||
// DnsServerModal, WarpModal, …) stage values behind Zod validation like
|
||||
// the clients/inbounds modals do, and some of their fields carry a
|
||||
// deliberate clear-means-zero semantic — the direct-write rule above
|
||||
// does not apply to them.
|
||||
files: ['src/pages/xray/**/*Modal.tsx'],
|
||||
rules: {
|
||||
'no-restricted-syntax': 'off',
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -1,26 +0,0 @@
|
||||
import tseslint from 'typescript-eslint';
|
||||
import reactHooks from 'eslint-plugin-react-hooks';
|
||||
|
||||
export default [
|
||||
{ ignores: ['node_modules/**', '../internal/web/dist/**', 'src/generated/**'] },
|
||||
{
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
plugins: {
|
||||
'@typescript-eslint': tseslint.plugin,
|
||||
'react-hooks': reactHooks,
|
||||
},
|
||||
languageOptions: {
|
||||
parser: tseslint.parser,
|
||||
parserOptions: {
|
||||
projectService: true,
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
'@typescript-eslint/no-deprecated': 'warn',
|
||||
},
|
||||
linterOptions: {
|
||||
reportUnusedDisableDirectives: 'off',
|
||||
},
|
||||
},
|
||||
];
|
||||
Generated
+1648
-2987
File diff suppressed because it is too large
Load Diff
+34
-33
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "3x-ui-frontend",
|
||||
"private": true,
|
||||
"version": "0.6.0",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"description": "3x-ui panel frontend (React 19 + Ant Design 6 + Vite 8).",
|
||||
"engines": {
|
||||
@@ -12,7 +12,11 @@
|
||||
"dev": "vite",
|
||||
"build": "npm run gen:api && vite build",
|
||||
"preview": "vite preview",
|
||||
"lint": "eslint src",
|
||||
"lint": "oxlint src tools",
|
||||
"lint:fix": "oxlint --fix src tools",
|
||||
"lint:deprecated": "oxlint --type-aware -A all -D typescript/no-deprecated src",
|
||||
"format": "oxfmt src tools",
|
||||
"format:check": "oxfmt --check src tools",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
@@ -24,64 +28,61 @@
|
||||
"prepare": "cd .. && husky frontend/.husky || true"
|
||||
},
|
||||
"lint-staged": {
|
||||
"src/**/*.{ts,tsx}": "eslint --fix"
|
||||
"src/**/*.{ts,tsx}": [
|
||||
"oxfmt",
|
||||
"oxlint --fix"
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^6.3.2",
|
||||
"@codemirror/lang-json": "^6.0.2",
|
||||
"@codemirror/theme-one-dark": "^6.1.3",
|
||||
"@hookform/resolvers": "^5.7.1",
|
||||
"@hookform/resolvers": "^5.9.1",
|
||||
"@noble/hashes": "^2.3.0",
|
||||
"@tanstack/react-query": "^5.101.4",
|
||||
"@tanstack/react-query-devtools": "^5.101.4",
|
||||
"antd": "^6.6.0",
|
||||
"@tanstack/react-query": "^5.102.2",
|
||||
"@tanstack/react-query-devtools": "^5.102.2",
|
||||
"antd": "^6.6.1",
|
||||
"codemirror": "^6.0.2",
|
||||
"dayjs": "^1.11.21",
|
||||
"i18next": "^26.3.6",
|
||||
"dayjs": "^1.11.23",
|
||||
"i18next": "^26.4.0",
|
||||
"otpauth": "^9.5.1",
|
||||
"persian-calendar-suite": "^1.5.6",
|
||||
"react": "^19.2.8",
|
||||
"react-dom": "^19.2.8",
|
||||
"react-hook-form": "^7.85.0",
|
||||
"react-i18next": "^17.0.11",
|
||||
"react-hook-form": "^7.86.0",
|
||||
"react-i18next": "^17.0.12",
|
||||
"react-router": "^8.3.0",
|
||||
"swagger-ui-react": "^5.32.13",
|
||||
"swagger-ui-react": "^5.32.14",
|
||||
"uplot": "^1.6.32",
|
||||
"zod": "^4.4.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@storybook/addon-a11y": "^10.5.7",
|
||||
"@storybook/addon-docs": "^10.5.7",
|
||||
"@storybook/addon-vitest": "^10.5.7",
|
||||
"@storybook/react-vite": "^10.5.7",
|
||||
"@storybook/addon-a11y": "^10.5.10",
|
||||
"@storybook/addon-docs": "^10.5.10",
|
||||
"@storybook/addon-vitest": "^10.5.10",
|
||||
"@storybook/react-vite": "^10.5.10",
|
||||
"@testing-library/dom": "^10.4.1",
|
||||
"@testing-library/react": "^16.3.2",
|
||||
"@types/react": "^19.2.18",
|
||||
"@types/react-dom": "^19.2.4",
|
||||
"@types/react-dom": "^19.2.5",
|
||||
"@types/swagger-ui-react": "^5.18.0",
|
||||
"@vitejs/plugin-react": "^6.0.5",
|
||||
"@vitest/browser-playwright": "4.1.10",
|
||||
"@vitest/coverage-v8": "^4.1.10",
|
||||
"eslint": "^10.8.1",
|
||||
"eslint-plugin-jsx-a11y": "^6.10.2",
|
||||
"eslint-plugin-react-hooks": "^7.1.1",
|
||||
"globals": "^17.11.0",
|
||||
"@vitejs/plugin-react": "^6.1.0",
|
||||
"@vitest/browser-playwright": "4.1.11",
|
||||
"@vitest/coverage-v8": "^4.1.11",
|
||||
"husky": "^9.1.7",
|
||||
"jsdom": "^30.0.1",
|
||||
"lint-staged": "^17.3.0",
|
||||
"msw": "^2.15.0",
|
||||
"oxfmt": "0.64.0",
|
||||
"oxlint": "1.79.0",
|
||||
"oxlint-tsgolint": "^7.0.2001",
|
||||
"playwright": "^1.62.1",
|
||||
"storybook": "^10.5.7",
|
||||
"typescript": "6.0.3",
|
||||
"typescript-eslint": "^8.67.0",
|
||||
"vite": "8.2.1",
|
||||
"vitest": "^4.1.10"
|
||||
"storybook": "^10.5.10",
|
||||
"typescript": "7.0.2",
|
||||
"vite": "8.2.2",
|
||||
"vitest": "^4.1.11"
|
||||
},
|
||||
"overrides": {
|
||||
"eslint-plugin-jsx-a11y": {
|
||||
"eslint": "$eslint"
|
||||
},
|
||||
"dompurify": "^3.4.11",
|
||||
"react-copy-to-clipboard": "^5.1.1",
|
||||
"react-inspector": "^9.0.0",
|
||||
|
||||
@@ -241,6 +241,9 @@
|
||||
"subJsonMux": {
|
||||
"type": "string"
|
||||
},
|
||||
"subJsonObservatory": {
|
||||
"type": "string"
|
||||
},
|
||||
"subJsonPath": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -438,6 +441,7 @@
|
||||
"subJsonEnable",
|
||||
"subJsonFinalMask",
|
||||
"subJsonMux",
|
||||
"subJsonObservatory",
|
||||
"subJsonPath",
|
||||
"subJsonRules",
|
||||
"subJsonURI",
|
||||
@@ -716,6 +720,9 @@
|
||||
"subJsonMux": {
|
||||
"type": "string"
|
||||
},
|
||||
"subJsonObservatory": {
|
||||
"type": "string"
|
||||
},
|
||||
"subJsonPath": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -920,6 +927,7 @@
|
||||
"subJsonEnable",
|
||||
"subJsonFinalMask",
|
||||
"subJsonMux",
|
||||
"subJsonObservatory",
|
||||
"subJsonPath",
|
||||
"subJsonRules",
|
||||
"subJsonURI",
|
||||
@@ -962,6 +970,36 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"AmneziaWGLogs": {
|
||||
"description": "AmneziaWGLogs is what the overview's AmneziaWG log view renders: the live\nper-peer activity of every running embedded interface, plus the panel's\nown recent AmneziaWG lifecycle log lines that explain a peer being absent\nfrom Peers at all.",
|
||||
"properties": {
|
||||
"events": {
|
||||
"example": [
|
||||
"2025/01/01 12:00:00 amneziawg: started interface awg1 for inbound 1"
|
||||
],
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"peers": {
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/PeerActivity"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"running": {
|
||||
"example": true,
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"events",
|
||||
"peers",
|
||||
"running"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ApiToken": {
|
||||
"properties": {
|
||||
"createdAt": {
|
||||
@@ -1056,6 +1094,16 @@
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"allowedIPsByInbound": {
|
||||
"additionalProperties": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"description": "AllowedIPsByInbound optionally overrides AllowedIPs on a per-inbound\nbasis, keyed by inbound id. Lets one identity attached to both\nWireGuard and AmneziaWG carry two genuinely different addresses in a\nsingle Create/Update call instead of the shared AllowedIPs field\nbeing broadcast to every attached tunnel inbound. Absent/unset for a\ngiven inbound id falls back to the shared AllowedIPs exactly as\nbefore -- fully backward compatible for callers that never set this.",
|
||||
"type": "object"
|
||||
},
|
||||
"auth": {
|
||||
"description": "Auth password (Hysteria)",
|
||||
"type": "string"
|
||||
@@ -1086,6 +1134,10 @@
|
||||
"description": "Flow control (XTLS)",
|
||||
"type": "string"
|
||||
},
|
||||
"forwardedPorts": {
|
||||
"description": "AmneziaWG per-client port-forwarding spec, e.g. \"80,443,8000-8100\"",
|
||||
"type": "string"
|
||||
},
|
||||
"group": {
|
||||
"description": "Logical grouping label",
|
||||
"type": "string"
|
||||
@@ -1250,6 +1302,9 @@
|
||||
"flow": {
|
||||
"type": "string"
|
||||
},
|
||||
"forwardedPorts": {
|
||||
"type": "string"
|
||||
},
|
||||
"group": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -1328,6 +1383,7 @@
|
||||
"enable",
|
||||
"expiryTime",
|
||||
"flow",
|
||||
"forwardedPorts",
|
||||
"group",
|
||||
"id",
|
||||
"keepAlive",
|
||||
@@ -2071,7 +2127,8 @@
|
||||
"mixed",
|
||||
"tunnel",
|
||||
"tun",
|
||||
"mtproto"
|
||||
"mtproto",
|
||||
"amneziawg"
|
||||
],
|
||||
"example": "vless",
|
||||
"type": "string"
|
||||
@@ -2223,6 +2280,15 @@
|
||||
},
|
||||
"InboundOption": {
|
||||
"properties": {
|
||||
"awgServer": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/ServerSettings"
|
||||
}
|
||||
],
|
||||
"description": "AwgServer carries the full AmneziaWG server block (keys, subnet,\nobfuscation params) so the clients page can render a downloadable\nper-client .conf without a second round trip.",
|
||||
"nullable": true
|
||||
},
|
||||
"enable": {
|
||||
"example": true,
|
||||
"type": "boolean"
|
||||
@@ -2905,6 +2971,68 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"PeerActivity": {
|
||||
"description": "PeerActivity is one peer's live embedded-Device-reported state, the\ncounterpart of an Xray access-log entry: a tunnel logs no requests, only\nhandshakes and bytes.",
|
||||
"properties": {
|
||||
"allowedIPs": {
|
||||
"example": "10.8.1.2/32",
|
||||
"type": "string"
|
||||
},
|
||||
"down": {
|
||||
"example": 4194304,
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"email": {
|
||||
"example": "peer@example.com",
|
||||
"type": "string"
|
||||
},
|
||||
"endpoint": {
|
||||
"example": "203.0.113.9:51820",
|
||||
"type": "string"
|
||||
},
|
||||
"handshake": {
|
||||
"description": "Handshake is unix milliseconds, 0 when the peer has never connected.",
|
||||
"example": 1735732800000,
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"inboundId": {
|
||||
"example": 1,
|
||||
"type": "integer"
|
||||
},
|
||||
"interface": {
|
||||
"example": "awg1",
|
||||
"type": "string"
|
||||
},
|
||||
"online": {
|
||||
"example": true,
|
||||
"type": "boolean"
|
||||
},
|
||||
"tag": {
|
||||
"example": "inbound-51820",
|
||||
"type": "string"
|
||||
},
|
||||
"up": {
|
||||
"example": 1048576,
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"allowedIPs",
|
||||
"down",
|
||||
"email",
|
||||
"endpoint",
|
||||
"handshake",
|
||||
"inboundId",
|
||||
"interface",
|
||||
"online",
|
||||
"tag",
|
||||
"up"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ProbeResultUI": {
|
||||
"properties": {
|
||||
"cpuPct": {
|
||||
@@ -3071,6 +3199,150 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ServerSettings": {
|
||||
"description": "ServerSettings is the \"server\" block of an AmneziaWG inbound's Settings\nJSON: the interface-level configuration shared by every client/peer. The\nlisten port is deliberately not duplicated here — it lives on the inbound\nrow itself (Inbound.Port), like every other protocol.",
|
||||
"properties": {
|
||||
"contentPaddingAddition": {
|
||||
"type": "string"
|
||||
},
|
||||
"disableCookies": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"externalInterface": {
|
||||
"description": "ExternalInterface, IPv6Enabled, and IPv6ExternalInterface are live\nagain as of Phase 3.5 -- see the matching fields on Instance for what\nthey gate (internal/amneziawgnet's IPv6-address-alias mechanism).\nIPv6Subnet was never actually vestigial either: InstanceFromInbound\nalready consumes it (via serverAddressV6) to build the server's own\ntunnel address, same as always. Only RouteThroughXray, below, remains\ngenuinely vestigial as of the hard cutover to the embedded path\n(internal/amneziawgnet) -- read from existing stored settings for\nbackward compatibility, but not acted on by anything.",
|
||||
"type": "string"
|
||||
},
|
||||
"h1": {
|
||||
"type": "string"
|
||||
},
|
||||
"h2": {
|
||||
"type": "string"
|
||||
},
|
||||
"h3": {
|
||||
"type": "string"
|
||||
},
|
||||
"h4": {
|
||||
"type": "string"
|
||||
},
|
||||
"headerProtectionKey": {
|
||||
"description": "HeaderProtectionKey and ContentPaddingAddition are AmneziaWG 3.0\nfields, flat and top-level for the same tools/openapigen reason as\nthe block above; Obfuscation() below folds them back into\nObfuscation31's own identically named fields.\nHeaderProtectionKey is a base64 32-byte key; empty (the default)\ndisables AWG 3.0 header protection. A non-empty value requires\nevery one of S1-S4 above to be >= 12 -- ValidateObfuscation\nenforces this at save time, not just at IpcSet time.\nContentPaddingAddition is a \"low-high\" range or bare integer, the\nsame grammar and uint32 cap as H1-H4.",
|
||||
"type": "string"
|
||||
},
|
||||
"i1": {
|
||||
"type": "string"
|
||||
},
|
||||
"i2": {
|
||||
"type": "string"
|
||||
},
|
||||
"i3": {
|
||||
"type": "string"
|
||||
},
|
||||
"i4": {
|
||||
"type": "string"
|
||||
},
|
||||
"i5": {
|
||||
"type": "string"
|
||||
},
|
||||
"ipv6Enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"ipv6ExternalInterface": {
|
||||
"type": "string"
|
||||
},
|
||||
"ipv6Subnet": {
|
||||
"type": "string"
|
||||
},
|
||||
"jc": {
|
||||
"description": "Obfuscation31's fields, repeated flat (not embedded) rather than\nnested under their own key: encoding/json would happily inline an\nembedded Obfuscation31 the same way, but the frontend's Go->Zod/TS\ngenerator (tools/openapigen) does not — it emits a genuinely nested\n`obfuscation31` object, which would silently diverge from the real\nwire JSON. See Obfuscation() below for the manager-facing conversion.",
|
||||
"type": "integer"
|
||||
},
|
||||
"jmax": {
|
||||
"type": "integer"
|
||||
},
|
||||
"jmin": {
|
||||
"type": "integer"
|
||||
},
|
||||
"keepaliveTimeout": {
|
||||
"type": "string"
|
||||
},
|
||||
"maxHandshakeAttempts": {
|
||||
"type": "string"
|
||||
},
|
||||
"mtu": {
|
||||
"type": "integer"
|
||||
},
|
||||
"primaryDns": {
|
||||
"description": "PrimaryDNS/SecondaryDNS seed client configs' DNS line. Blank is\nmeaningful, so no omitempty: a dropped key resurrects frontend defaults.",
|
||||
"type": "string"
|
||||
},
|
||||
"privateKey": {
|
||||
"type": "string"
|
||||
},
|
||||
"publicKey": {
|
||||
"type": "string"
|
||||
},
|
||||
"randomTrailers": {
|
||||
"description": "RandomTrailers/DisableCookies mirror Instance's identically named\nAmneziaWG 3.1 fields -- see that type's own doc comment for the real\nprotocol/interop details. Both real bool fields (not omitempty):\nbuildUAPIConfig always emits both lines explicitly so the\nreconfigure-in-place diff correctly notices a true->false edit, not\njust false->true.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"rejectAfterTime": {
|
||||
"type": "string"
|
||||
},
|
||||
"rekeyAfterTime": {
|
||||
"description": "RekeyAfterTime/RekeyTimeout/RejectAfterTime/KeepaliveTimeout/\nMaxHandshakeAttempts mirror Instance's identically named fields --\nsee that type's own doc comment for the grammar/width/real-default\ndetails. Flat and top-level for the same tools/openapigen reason as\nthe rest of this struct.",
|
||||
"type": "string"
|
||||
},
|
||||
"rekeyTimeout": {
|
||||
"type": "string"
|
||||
},
|
||||
"routeThroughXray": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"s1": {
|
||||
"type": "integer"
|
||||
},
|
||||
"s2": {
|
||||
"type": "integer"
|
||||
},
|
||||
"s3": {
|
||||
"type": "integer"
|
||||
},
|
||||
"s4": {
|
||||
"type": "integer"
|
||||
},
|
||||
"secondaryDns": {
|
||||
"type": "string"
|
||||
},
|
||||
"subnetCidr": {
|
||||
"type": "integer"
|
||||
},
|
||||
"subnetIp": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"disableCookies",
|
||||
"h1",
|
||||
"h2",
|
||||
"h3",
|
||||
"h4",
|
||||
"jc",
|
||||
"jmax",
|
||||
"jmin",
|
||||
"primaryDns",
|
||||
"privateKey",
|
||||
"publicKey",
|
||||
"randomTrailers",
|
||||
"s1",
|
||||
"s2",
|
||||
"s3",
|
||||
"s4",
|
||||
"secondaryDns",
|
||||
"subnetCidr",
|
||||
"subnetIp"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"Setting": {
|
||||
"description": "Setting stores key-value configuration settings for the 3x-ui panel.",
|
||||
"properties": {
|
||||
@@ -3091,6 +3363,71 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"SubBalancer": {
|
||||
"description": "SubBalancer is one extra JSON-subscription config document whose members are\nthe selected inbounds' proxy outbounds. SortOrder shares SubSortIndex semantics.",
|
||||
"properties": {
|
||||
"createdAt": {
|
||||
"example": 1710000000000,
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"enabled": {
|
||||
"description": "No gorm default:true — a bool default makes an explicit false at insert\ncollapse back to the column default (zero value is skipped).",
|
||||
"example": true,
|
||||
"type": "boolean"
|
||||
},
|
||||
"id": {
|
||||
"example": 1,
|
||||
"type": "integer"
|
||||
},
|
||||
"inboundIds": {
|
||||
"example": [
|
||||
1,
|
||||
3
|
||||
],
|
||||
"items": {
|
||||
"type": "integer"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"remark": {
|
||||
"example": "auto-fastest",
|
||||
"maxLength": 256,
|
||||
"type": "string"
|
||||
},
|
||||
"sortOrder": {
|
||||
"example": 1,
|
||||
"minimum": 1,
|
||||
"type": "integer"
|
||||
},
|
||||
"strategy": {
|
||||
"enum": [
|
||||
"leastLoad",
|
||||
"leastPing",
|
||||
"random",
|
||||
"roundRobin"
|
||||
],
|
||||
"example": "random",
|
||||
"type": "string"
|
||||
},
|
||||
"updatedAt": {
|
||||
"example": 1710000000000,
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"createdAt",
|
||||
"enabled",
|
||||
"id",
|
||||
"inboundIds",
|
||||
"remark",
|
||||
"sortOrder",
|
||||
"strategy",
|
||||
"updatedAt"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"User": {
|
||||
"description": "User represents a user account in the 3x-ui panel.",
|
||||
"properties": {
|
||||
@@ -3160,7 +3497,11 @@
|
||||
},
|
||||
{
|
||||
"name": "Xray Settings",
|
||||
"description": "Xray configuration template, outbound management, Warp/Nord integration, and config testing. All endpoints under /panel/api/xray."
|
||||
"description": "Xray configuration template, outbound management, Warp/Nord/PIA integration, and config testing. All endpoints under /panel/api/xray."
|
||||
},
|
||||
{
|
||||
"name": "Subscription Balancers",
|
||||
"description": "Client-side balancers for the JSON subscription: each enabled balancer is emitted as one extra config document whose members are the proxy outbounds of the selected inbounds (routing.balancers + burstObservatory). Managed in Settings → Sub Balancers."
|
||||
},
|
||||
{
|
||||
"name": "Subscription Server",
|
||||
@@ -3531,6 +3872,7 @@
|
||||
"success": true,
|
||||
"obj": [
|
||||
{
|
||||
"awgServer": null,
|
||||
"enable": true,
|
||||
"id": 1,
|
||||
"listen": "",
|
||||
@@ -5583,6 +5925,82 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/panel/api/server/amneziawglogs/{count}": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"Server"
|
||||
],
|
||||
"summary": "Return live AmneziaWG peer activity (handshake, endpoint, transfer) plus the panel’s own AmneziaWG event lines.",
|
||||
"operationId": "post_panel_api_server_amneziawglogs_count",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "count",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"description": "Maximum peer rows and event lines to return.",
|
||||
"schema": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"success": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"msg": {
|
||||
"type": "string"
|
||||
},
|
||||
"obj": {
|
||||
"$ref": "#/components/schemas/AmneziaWGLogs"
|
||||
}
|
||||
}
|
||||
},
|
||||
"example": {
|
||||
"success": true,
|
||||
"obj": {
|
||||
"events": [
|
||||
"2025/01/01 12:00:00 amneziawg: started interface awg1 for inbound 1"
|
||||
],
|
||||
"peers": [
|
||||
{
|
||||
"allowedIPs": "10.8.1.2/32",
|
||||
"down": 4194304,
|
||||
"email": "peer@example.com",
|
||||
"endpoint": "203.0.113.9:51820",
|
||||
"handshake": 1735732800000,
|
||||
"inboundId": 1,
|
||||
"interface": "awg1",
|
||||
"online": true,
|
||||
"tag": "inbound-51820",
|
||||
"up": 1048576
|
||||
}
|
||||
],
|
||||
"running": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/panel/api/server/importDB": {
|
||||
"post": {
|
||||
"tags": [
|
||||
@@ -6245,8 +6663,9 @@
|
||||
"tags": [
|
||||
"Clients"
|
||||
],
|
||||
"summary": "Create a new client and attach it to one or more inbounds in a single call. Body is JSON. Per-protocol secrets (UUID for VLESS/VMess, password for Trojan/Shadowsocks, auth for Hysteria) are generated server-side when omitted, so callers can send only the universal fields.",
|
||||
"summary": "Create a new client and attach it to one or more inbounds in a single call. Body is JSON. Per-protocol secrets are generated server-side when omitted, so callers can send only the universal fields.",
|
||||
"operationId": "post_panel_api_clients_add",
|
||||
"description": "Fields the server fills in when they are omitted — a valid value sent by the caller is never overwritten. Re-adding an email that already exists, with its stored `subId`, reuses the stored `id`, `password`, `auth` and `secret` instead of minting new ones, so the identity stays in sync across its inbounds.\n\n- **VLESS / VMess** — `id`, a fresh UUID\n- **Trojan** — `password`\n- **Shadowsocks** — `password`. On a `2022-blake3-*` inbound a supplied password that does not base64-decode to the key length of the cipher (16 or 32 bytes) is replaced by a generated key and the call still succeeds, so read the client back if you did not let the server pick. Legacy ciphers keep any non-empty password\n- **Hysteria** — `auth`\n- **mtproto** — `secret`, a FakeTLS secret derived from the fronting domain of the inbound, or from `www.cloudflare.com` when it has none\n- **WireGuard** — `privateKey` and `publicKey` when both are blank, or `publicKey` alone when only a `privateKey` was sent, plus `allowedIPs`: one free `/32` taken from the /24 the existing peers of that inbound already sit in, or from `10.0.0.0/24` when it has none\n\nAccepted on the same body but never generated: `preSharedKey` and `keepAlive` (WireGuard), `adTag` (mtproto).\n\nWireGuard is the only one of these that can fail. Allocation widens the search to the containing /16 before giving up with `wireguard: no free address available in <scope>`, and an `allowedIPs` supplied by the caller is validated instead of allocated: `wireguard: allowedIPs entry already used by another client: <address>` when a different client of that same inbound already holds it. The check is per inbound, so the same address on two different inbounds is accepted. The same validation runs on POST /panel/api/clients/{email}/attach, where a client that already carries an address brings it along.",
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
@@ -6423,6 +6842,7 @@
|
||||
],
|
||||
"summary": "Attach an existing client to one or more additional inbounds. Body is JSON.",
|
||||
"operationId": "post_panel_api_clients_email_attach",
|
||||
"description": "A WireGuard client brings its stored `allowedIPs` into the new inbound instead of being given a fresh address, so the call fails with `wireguard: allowedIPs entry already used by another client: <address>` when a different client of the target inbound already holds it. Free the address on that inbound first — see POST /panel/api/clients/add for the full rule.",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "email",
|
||||
@@ -7987,6 +8407,56 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/panel/api/clients/hwids/{email}/{id}": {
|
||||
"delete": {
|
||||
"tags": [
|
||||
"Clients"
|
||||
],
|
||||
"summary": "Remove a single registered HWID device by its id, freeing one slot under the HWID limit.",
|
||||
"operationId": "delete_panel_api_clients_hwids_email_id",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "email",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"description": "Client email.",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"description": "Device id, from the list endpoint.",
|
||||
"schema": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"success": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"msg": {
|
||||
"type": "string"
|
||||
},
|
||||
"obj": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/panel/api/clients/onlines": {
|
||||
"post": {
|
||||
"tags": [
|
||||
@@ -11091,6 +11561,47 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/panel/api/xray/pia/{action}": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"Xray Settings"
|
||||
],
|
||||
"summary": "Manage PIA WireGuard integration. The action parameter selects the operation.",
|
||||
"operationId": "post_panel_api_xray_pia_action",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "action",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"description": "countries — list available countries from the signed PIA server list. servers — list regions and WireGuard servers in a country (sends countryCode). reg — sign in with a PIA username and password (sends username, password). data — return the signed-in account hint. del — delete stored PIA credentials. addKey — register a WireGuard key with the selected server (sends hostname) and return fields to build the outbound.",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"success": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"msg": {
|
||||
"type": "string"
|
||||
},
|
||||
"obj": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/panel/api/xray/resetOutboundsTraffic": {
|
||||
"post": {
|
||||
"tags": [
|
||||
@@ -11837,6 +12348,280 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/panel/api/sub-balancers": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Subscription Balancers"
|
||||
],
|
||||
"summary": "List all subscription balancers in sort order (sort_order asc, id asc).",
|
||||
"operationId": "get_panel_api_sub_balancers",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"success": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"msg": {
|
||||
"type": "string"
|
||||
},
|
||||
"obj": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/SubBalancer"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"example": {
|
||||
"success": true,
|
||||
"obj": [
|
||||
{
|
||||
"createdAt": 1710000000000,
|
||||
"enabled": true,
|
||||
"id": 1,
|
||||
"inboundIds": [
|
||||
1,
|
||||
3
|
||||
],
|
||||
"remark": "auto-fastest",
|
||||
"sortOrder": 1,
|
||||
"strategy": "random",
|
||||
"updatedAt": 1710000000000
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
"tags": [
|
||||
"Subscription Balancers"
|
||||
],
|
||||
"summary": "Create a subscription balancer. It appears in the JSON subscription of every client that sits on at least one selected inbound.",
|
||||
"operationId": "post_panel_api_sub_balancers",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"success": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"msg": {
|
||||
"type": "string"
|
||||
},
|
||||
"obj": {
|
||||
"$ref": "#/components/schemas/SubBalancer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"example": {
|
||||
"success": true,
|
||||
"obj": {
|
||||
"createdAt": 1710000000000,
|
||||
"enabled": true,
|
||||
"id": 1,
|
||||
"inboundIds": [
|
||||
1,
|
||||
3
|
||||
],
|
||||
"remark": "auto-fastest",
|
||||
"sortOrder": 1,
|
||||
"strategy": "random",
|
||||
"updatedAt": 1710000000000
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/panel/api/sub-balancers/{id}": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"Subscription Balancers"
|
||||
],
|
||||
"summary": "Update a balancer by id. Accepts the same form fields as create (full-row update, including the enabled toggle).",
|
||||
"operationId": "post_panel_api_sub_balancers_id",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"description": "Balancer id.",
|
||||
"schema": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"success": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"msg": {
|
||||
"type": "string"
|
||||
},
|
||||
"obj": {
|
||||
"$ref": "#/components/schemas/SubBalancer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"example": {
|
||||
"success": true,
|
||||
"obj": {
|
||||
"createdAt": 1710000000000,
|
||||
"enabled": true,
|
||||
"id": 1,
|
||||
"inboundIds": [
|
||||
1,
|
||||
3
|
||||
],
|
||||
"remark": "auto-fastest",
|
||||
"sortOrder": 1,
|
||||
"strategy": "random",
|
||||
"updatedAt": 1710000000000
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"tags": [
|
||||
"Subscription Balancers"
|
||||
],
|
||||
"summary": "Delete a balancer by id.",
|
||||
"operationId": "delete_panel_api_sub_balancers_id",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"description": "Balancer id.",
|
||||
"schema": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"success": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"msg": {
|
||||
"type": "string"
|
||||
},
|
||||
"obj": {
|
||||
"$ref": "#/components/schemas/SubBalancer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"example": {
|
||||
"success": true,
|
||||
"obj": {
|
||||
"createdAt": 1710000000000,
|
||||
"enabled": true,
|
||||
"id": 1,
|
||||
"inboundIds": [
|
||||
1,
|
||||
3
|
||||
],
|
||||
"remark": "auto-fastest",
|
||||
"sortOrder": 1,
|
||||
"strategy": "random",
|
||||
"updatedAt": 1710000000000
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/panel/api/sub-balancers/{id}/del": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"Subscription Balancers"
|
||||
],
|
||||
"summary": "Delete a balancer by id (POST alias of DELETE for clients that cannot send DELETE).",
|
||||
"operationId": "post_panel_api_sub_balancers_id_del",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"description": "Balancer id.",
|
||||
"schema": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"success": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"msg": {
|
||||
"type": "string"
|
||||
},
|
||||
"obj": {
|
||||
"$ref": "#/components/schemas/SubBalancer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"example": {
|
||||
"success": true,
|
||||
"obj": {
|
||||
"createdAt": 1710000000000,
|
||||
"enabled": true,
|
||||
"id": 1,
|
||||
"inboundIds": [
|
||||
1,
|
||||
3
|
||||
],
|
||||
"remark": "auto-fastest",
|
||||
"sortOrder": 1,
|
||||
"strategy": "random",
|
||||
"updatedAt": 1710000000000
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/{subPath}{subid}": {
|
||||
"get": {
|
||||
"tags": [
|
||||
|
||||
@@ -79,7 +79,9 @@ function encodeForm(data: unknown): string {
|
||||
return;
|
||||
}
|
||||
if (typeof value === 'object') {
|
||||
Object.entries(value as Record<string, unknown>).forEach(([k, v]) => append(`${key}[${k}]`, v));
|
||||
Object.entries(value as Record<string, unknown>).forEach(([k, v]) =>
|
||||
append(`${key}[${k}]`, v),
|
||||
);
|
||||
return;
|
||||
}
|
||||
parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`);
|
||||
|
||||
@@ -4,7 +4,11 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { HttpUtil, Msg } from '@/utils';
|
||||
import { parseMsg } from '@/utils/zodValidate';
|
||||
import { AllSetting } from '@/models/setting';
|
||||
import { AllSettingResponseSchema, AllSettingSchema, type AllSettingInput } from '@/schemas/setting';
|
||||
import {
|
||||
AllSettingResponseSchema,
|
||||
AllSettingSchema,
|
||||
type AllSettingInput,
|
||||
} from '@/schemas/setting';
|
||||
import { keys } from '@/api/queryKeys';
|
||||
import { useServerDraft } from '@/hooks/useServerDraft';
|
||||
|
||||
@@ -39,28 +43,41 @@ export function useAllSettings() {
|
||||
);
|
||||
const allSetting = draft ?? server;
|
||||
|
||||
const updateSetting = useCallback((patch: Partial<AllSetting>) => {
|
||||
setDraft((prev) => {
|
||||
const next = new AllSetting(prev ?? server);
|
||||
Object.assign(next, patch);
|
||||
return next;
|
||||
});
|
||||
}, [server, setDraft]);
|
||||
const updateSetting = useCallback(
|
||||
(patch: Partial<AllSetting>) => {
|
||||
setDraft((prev) => {
|
||||
const next = new AllSetting(prev ?? server);
|
||||
Object.assign(next, patch);
|
||||
return next;
|
||||
});
|
||||
},
|
||||
[server, setDraft],
|
||||
);
|
||||
|
||||
const saveMut = useMutation({
|
||||
mutationFn: async ({ payload, saved }: { payload: SettingSavePayload; saved?: AllSetting }): Promise<SettingSaveResult> => {
|
||||
mutationFn: async ({
|
||||
payload,
|
||||
saved,
|
||||
}: {
|
||||
payload: SettingSavePayload;
|
||||
saved?: AllSetting;
|
||||
}): Promise<SettingSaveResult> => {
|
||||
const next = { ...payload };
|
||||
const body = AllSettingSchema.partial().safeParse(next);
|
||||
if (!body.success) {
|
||||
console.warn('[zod] setting/update body failed validation', body.error.issues);
|
||||
}
|
||||
const msg = await HttpUtil.post('/panel/api/setting/update', body.success ? { ...next, ...body.data } : next);
|
||||
const msg = await HttpUtil.post(
|
||||
'/panel/api/setting/update',
|
||||
body.success ? { ...next, ...body.data } : next,
|
||||
);
|
||||
return { msg, saved };
|
||||
},
|
||||
onSuccess: ({ msg, saved }) => {
|
||||
if (!msg?.success) return;
|
||||
if (saved) markSaved(saved);
|
||||
queryClient.invalidateQueries({ queryKey: keys.settings.all() });
|
||||
queryClient.invalidateQueries({ queryKey: keys.settings.defaults() });
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { HttpUtil } from '@/utils';
|
||||
import { parseMsg } from '@/utils/zodValidate';
|
||||
import { keys } from '@/api/queryKeys';
|
||||
import { ClientRecordSchema, type ClientRecord } from '@/schemas/client';
|
||||
|
||||
const ClientRecordListSchema = z
|
||||
.array(ClientRecordSchema)
|
||||
.nullable()
|
||||
.transform((value) => value ?? []);
|
||||
|
||||
async function fetchClients(): Promise<ClientRecord[]> {
|
||||
const msg = await HttpUtil.get('/panel/api/clients/list', undefined, { silent: true });
|
||||
if (!msg?.success) throw new Error(msg?.msg || 'Failed to load clients');
|
||||
const validated = parseMsg(msg, ClientRecordListSchema, 'clients/list');
|
||||
return validated.obj ?? [];
|
||||
}
|
||||
|
||||
export function useClientOptions(enabled = true) {
|
||||
return useQuery({
|
||||
queryKey: keys.clients.all(),
|
||||
queryFn: fetchClients,
|
||||
enabled,
|
||||
staleTime: 30_000,
|
||||
select: (clients) =>
|
||||
clients
|
||||
.map((client) => client.email.trim())
|
||||
.filter(Boolean)
|
||||
.sort((a, b) => a.localeCompare(b)),
|
||||
});
|
||||
}
|
||||
@@ -6,7 +6,9 @@ import { FactoryDefaultsSchema, type FactoryDefaults } from '@/schemas/setting';
|
||||
import { keys } from '@/api/queryKeys';
|
||||
|
||||
async function fetchFactoryDefaults(): Promise<FactoryDefaults> {
|
||||
const msg = await HttpUtil.post('/panel/api/setting/factoryDefaults', undefined, { silent: true });
|
||||
const msg = await HttpUtil.post('/panel/api/setting/factoryDefaults', undefined, {
|
||||
silent: true,
|
||||
});
|
||||
if (!msg?.success) throw new Error(msg?.msg || 'Failed to fetch factory defaults');
|
||||
const validated = parseMsg(msg, FactoryDefaultsSchema, 'setting/factoryDefaults');
|
||||
const parsed = FactoryDefaultsSchema.safeParse(validated.obj);
|
||||
|
||||
@@ -18,7 +18,9 @@ const FAIL_OPEN_STATUS: Fail2banStatus = {
|
||||
};
|
||||
|
||||
async function fetchFail2banStatus(): Promise<Fail2banStatus> {
|
||||
const msg = await HttpUtil.get<Fail2banStatus>('/panel/api/server/fail2banStatus', undefined, { silent: true });
|
||||
const msg = await HttpUtil.get<Fail2banStatus>('/panel/api/server/fail2banStatus', undefined, {
|
||||
silent: true,
|
||||
});
|
||||
if (!msg?.success || !msg.obj) throw new Error(msg?.msg || 'Failed to fetch fail2ban status');
|
||||
return { ...FAIL_OPEN_STATUS, ...msg.obj };
|
||||
}
|
||||
|
||||
@@ -2,7 +2,12 @@ import { keepPreviousData, useMutation, useQuery } from '@tanstack/react-query';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { keys } from '@/api/queryKeys';
|
||||
import { GeoCategoryPageSchema, GeoEntryPageSchema, GeoFileSchema, GeodataTokenIssueSchema } from '@/generated/zod';
|
||||
import {
|
||||
GeoCategoryPageSchema,
|
||||
GeoEntryPageSchema,
|
||||
GeoFileSchema,
|
||||
GeodataTokenIssueSchema,
|
||||
} from '@/generated/zod';
|
||||
import type { GeoCategoryPage, GeoEntryPage, GeoFile, GeodataTokenIssue } from '@/generated/types';
|
||||
import { HttpUtil } from '@/utils';
|
||||
import { parseMsg } from '@/utils/zodValidate';
|
||||
@@ -28,7 +33,11 @@ async function fetchGeodataFiles(): Promise<GeoFile[]> {
|
||||
}
|
||||
|
||||
async function fetchGeodataCategories(file: string, query: string): Promise<GeoCategoryPage> {
|
||||
const msg = await HttpUtil.get('/panel/api/xray/geodata/categories', { file, q: query }, { silent: true });
|
||||
const msg = await HttpUtil.get(
|
||||
'/panel/api/xray/geodata/categories',
|
||||
{ file, q: query },
|
||||
{ silent: true },
|
||||
);
|
||||
if (!msg?.success) throw new Error(msg?.msg || 'Failed to fetch geodata categories');
|
||||
const validated = parseMsg(msg, GeoCategoryPageSchema, 'xray/geodata/categories');
|
||||
return validated.obj ?? EMPTY_CATEGORY_PAGE;
|
||||
|
||||
@@ -11,50 +11,69 @@ export function useHostMutations() {
|
||||
const invalidate = () => queryClient.invalidateQueries({ queryKey: keys.hosts.root() });
|
||||
|
||||
const bulkCreateMut = useMutation({
|
||||
mutationFn: (payload: BulkAddHostValues) => HttpUtil.post('/panel/api/hosts/bulk/add', payload, JSON_HEADERS),
|
||||
onSuccess: (msg) => { if (msg?.success) invalidate(); },
|
||||
mutationFn: (payload: BulkAddHostValues) =>
|
||||
HttpUtil.post('/panel/api/hosts/bulk/add', payload, JSON_HEADERS),
|
||||
onSuccess: (msg) => {
|
||||
if (msg?.success) invalidate();
|
||||
},
|
||||
});
|
||||
|
||||
const updateMut = useMutation({
|
||||
mutationFn: ({ groupId, payload }: { groupId: string; payload: BulkAddHostValues }) =>
|
||||
HttpUtil.post(`/panel/api/hosts/update/${groupId}`, payload, JSON_HEADERS),
|
||||
onSuccess: (msg) => { if (msg?.success) invalidate(); },
|
||||
onSuccess: (msg) => {
|
||||
if (msg?.success) invalidate();
|
||||
},
|
||||
});
|
||||
|
||||
const removeMut = useMutation({
|
||||
mutationFn: (groupId: string) => HttpUtil.post(`/panel/api/hosts/del/${groupId}`),
|
||||
onSuccess: (msg) => { if (msg?.success) invalidate(); },
|
||||
onSuccess: (msg) => {
|
||||
if (msg?.success) invalidate();
|
||||
},
|
||||
});
|
||||
|
||||
const setEnableMut = useMutation({
|
||||
mutationFn: ({ groupId, enable }: { groupId: string; enable: boolean }) =>
|
||||
HttpUtil.post(`/panel/api/hosts/setEnable/${groupId}`, { enable }),
|
||||
onSuccess: (msg) => { if (msg?.success) invalidate(); },
|
||||
onSuccess: (msg) => {
|
||||
if (msg?.success) invalidate();
|
||||
},
|
||||
});
|
||||
|
||||
const reorderMut = useMutation({
|
||||
mutationFn: (groupIds: string[]) => HttpUtil.post('/panel/api/hosts/reorder', { ids: groupIds }, JSON_HEADERS),
|
||||
onSuccess: (msg) => { if (msg?.success) invalidate(); },
|
||||
mutationFn: (groupIds: string[]) =>
|
||||
HttpUtil.post('/panel/api/hosts/reorder', { ids: groupIds }, JSON_HEADERS),
|
||||
onSuccess: (msg) => {
|
||||
if (msg?.success) invalidate();
|
||||
},
|
||||
});
|
||||
|
||||
const bulkEnableMut = useMutation({
|
||||
mutationFn: ({ groupIds, enable }: { groupIds: string[]; enable: boolean }) =>
|
||||
HttpUtil.post('/panel/api/hosts/bulk/setEnable', { ids: groupIds, enable }, JSON_HEADERS),
|
||||
onSuccess: (msg) => { if (msg?.success) invalidate(); },
|
||||
onSuccess: (msg) => {
|
||||
if (msg?.success) invalidate();
|
||||
},
|
||||
});
|
||||
|
||||
const bulkDelMut = useMutation({
|
||||
mutationFn: (groupIds: string[]) => HttpUtil.post('/panel/api/hosts/bulk/del', { ids: groupIds }, JSON_HEADERS),
|
||||
onSuccess: (msg) => { if (msg?.success) invalidate(); },
|
||||
mutationFn: (groupIds: string[]) =>
|
||||
HttpUtil.post('/panel/api/hosts/bulk/del', { ids: groupIds }, JSON_HEADERS),
|
||||
onSuccess: (msg) => {
|
||||
if (msg?.success) invalidate();
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
bulkCreate: (payload: BulkAddHostValues) => bulkCreateMut.mutateAsync(payload),
|
||||
update: (groupId: string, payload: BulkAddHostValues) => updateMut.mutateAsync({ groupId, payload }),
|
||||
update: (groupId: string, payload: BulkAddHostValues) =>
|
||||
updateMut.mutateAsync({ groupId, payload }),
|
||||
remove: (groupId: string) => removeMut.mutateAsync(groupId),
|
||||
setEnable: (groupId: string, enable: boolean) => setEnableMut.mutateAsync({ groupId, enable }),
|
||||
reorder: (groupIds: string[]) => reorderMut.mutateAsync(groupIds),
|
||||
bulkSetEnable: (groupIds: string[], enable: boolean) => bulkEnableMut.mutateAsync({ groupIds, enable }),
|
||||
bulkSetEnable: (groupIds: string[], enable: boolean) =>
|
||||
bulkEnableMut.mutateAsync({ groupIds, enable }),
|
||||
bulkDel: (groupIds: string[]) => bulkDelMut.mutateAsync(groupIds),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -30,27 +30,33 @@ export function useNodeMutations() {
|
||||
};
|
||||
|
||||
const createMut = useMutation({
|
||||
mutationFn: (payload: Partial<NodeRecord>) =>
|
||||
HttpUtil.post('/panel/api/nodes/add', payload),
|
||||
onSuccess: (msg) => { if (msg?.success) invalidate(); },
|
||||
mutationFn: (payload: Partial<NodeRecord>) => HttpUtil.post('/panel/api/nodes/add', payload),
|
||||
onSuccess: (msg) => {
|
||||
if (msg?.success) invalidate();
|
||||
},
|
||||
});
|
||||
|
||||
const updateMut = useMutation({
|
||||
mutationFn: ({ id, payload }: { id: number; payload: Partial<NodeRecord> }) =>
|
||||
HttpUtil.post(`/panel/api/nodes/update/${id}`, payload),
|
||||
onSuccess: (msg) => { if (msg?.success) invalidate(); },
|
||||
onSuccess: (msg) => {
|
||||
if (msg?.success) invalidate();
|
||||
},
|
||||
});
|
||||
|
||||
const removeMut = useMutation({
|
||||
mutationFn: (id: number) =>
|
||||
HttpUtil.post(`/panel/api/nodes/del/${id}`),
|
||||
onSuccess: (msg) => { if (msg?.success) invalidate(); },
|
||||
mutationFn: (id: number) => HttpUtil.post(`/panel/api/nodes/del/${id}`),
|
||||
onSuccess: (msg) => {
|
||||
if (msg?.success) invalidate();
|
||||
},
|
||||
});
|
||||
|
||||
const setEnableMut = useMutation({
|
||||
mutationFn: ({ id, enable }: { id: number; enable: boolean }) =>
|
||||
HttpUtil.post(`/panel/api/nodes/setEnable/${id}`, { enable }),
|
||||
onSuccess: (msg) => { if (msg?.success) invalidate(); },
|
||||
onSuccess: (msg) => {
|
||||
if (msg?.success) invalidate();
|
||||
},
|
||||
});
|
||||
|
||||
const probeMut = useMutation({
|
||||
@@ -58,15 +64,23 @@ export function useNodeMutations() {
|
||||
const raw = await HttpUtil.post(`/panel/api/nodes/probe/${id}`);
|
||||
return parseMsg(raw, ProbeResultSchema, 'nodes/probe');
|
||||
},
|
||||
onSuccess: (msg) => { if (msg?.success) invalidate(); },
|
||||
onSuccess: (msg) => {
|
||||
if (msg?.success) invalidate();
|
||||
},
|
||||
});
|
||||
|
||||
const updatePanelsMut = useMutation({
|
||||
mutationFn: ({ ids, dev }: { ids: number[]; dev: boolean }) =>
|
||||
HttpUtil.post<NodeUpdateResult[]>('/panel/api/nodes/updatePanel', { ids, dev }, {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}),
|
||||
onSuccess: (msg) => { if (msg?.success) invalidate(); },
|
||||
HttpUtil.post<NodeUpdateResult[]>(
|
||||
'/panel/api/nodes/updatePanel',
|
||||
{ ids, dev },
|
||||
{
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
},
|
||||
),
|
||||
onSuccess: (msg) => {
|
||||
if (msg?.success) invalidate();
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
@@ -75,7 +89,8 @@ export function useNodeMutations() {
|
||||
remove: (id: number) => removeMut.mutateAsync(id),
|
||||
setEnable: (id: number, enable: boolean) => setEnableMut.mutateAsync({ id, enable }),
|
||||
probe: (id: number) => probeMut.mutateAsync(id),
|
||||
updatePanels: (ids: number[], dev: boolean): Promise<Msg<NodeUpdateResult[]>> => updatePanelsMut.mutateAsync({ ids, dev }),
|
||||
updatePanels: (ids: number[], dev: boolean): Promise<Msg<NodeUpdateResult[]>> =>
|
||||
updatePanelsMut.mutateAsync({ ids, dev }),
|
||||
testConnection: async (payload: Partial<NodeRecord>): Promise<Msg<ProbeResult>> => {
|
||||
const raw = await HttpUtil.post('/panel/api/nodes/test', payload);
|
||||
return parseMsg(raw, ProbeResultSchema, 'nodes/test');
|
||||
|
||||
@@ -26,7 +26,9 @@ export function useOutboundTags(opts?: { excludeBlackhole?: boolean }) {
|
||||
}
|
||||
// Balancers are valid routing targets too — injectMtprotoEgress emits a
|
||||
// balancerTag rule when the chosen tag names a balancer.
|
||||
const balancers = (data?.xraySetting?.routing as { balancers?: Array<{ tag?: string }> } | undefined)?.balancers;
|
||||
const balancers = (
|
||||
data?.xraySetting?.routing as { balancers?: Array<{ tag?: string }> } | undefined
|
||||
)?.balancers;
|
||||
for (const b of balancers ?? []) {
|
||||
if (b?.tag) tags.add(b.tag);
|
||||
}
|
||||
@@ -61,7 +63,9 @@ export function useOutboundTagGroups(opts?: { excludeBlackhole?: boolean }) {
|
||||
if (t) outbounds.add(t);
|
||||
}
|
||||
const balancers: string[] = [];
|
||||
const bal = (data?.xraySetting?.routing as { balancers?: Array<{ tag?: string }> } | undefined)?.balancers;
|
||||
const bal = (
|
||||
data?.xraySetting?.routing as { balancers?: Array<{ tag?: string }> } | undefined
|
||||
)?.balancers;
|
||||
for (const b of bal ?? []) {
|
||||
if (b?.tag && !outbounds.has(b.tag)) balancers.push(b.tag);
|
||||
}
|
||||
|
||||
@@ -26,7 +26,9 @@ export function useStatusQuery() {
|
||||
});
|
||||
|
||||
const status = useMemo(() => query.data ?? new Status(), [query.data]);
|
||||
const refresh = async () => { await query.refetch(); };
|
||||
const refresh = async () => {
|
||||
await query.refetch();
|
||||
};
|
||||
|
||||
return {
|
||||
status,
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
import { HttpUtil } from '@/utils';
|
||||
import { keys } from '@/api/queryKeys';
|
||||
import type { SubBalancerFormValues } from '@/schemas/subBalancer';
|
||||
|
||||
// Deliberately urlencoded (no JSON headers): the Go side binds inboundIds from
|
||||
// repeated form keys, which is exactly how HttpUtil encodes arrays.
|
||||
export function useSubBalancerMutations() {
|
||||
const queryClient = useQueryClient();
|
||||
const invalidate = () => queryClient.invalidateQueries({ queryKey: keys.subBalancers.root() });
|
||||
|
||||
const createMut = useMutation({
|
||||
mutationFn: (payload: SubBalancerFormValues) =>
|
||||
HttpUtil.post('/panel/api/sub-balancers', payload),
|
||||
onSuccess: (msg) => {
|
||||
if (msg?.success) invalidate();
|
||||
},
|
||||
});
|
||||
|
||||
const updateMut = useMutation({
|
||||
mutationFn: ({ id, payload }: { id: number; payload: SubBalancerFormValues }) =>
|
||||
HttpUtil.post(`/panel/api/sub-balancers/${id}`, payload),
|
||||
onSuccess: (msg) => {
|
||||
if (msg?.success) invalidate();
|
||||
},
|
||||
});
|
||||
|
||||
const removeMut = useMutation({
|
||||
mutationFn: (id: number) => HttpUtil.post(`/panel/api/sub-balancers/${id}/del`),
|
||||
onSuccess: (msg) => {
|
||||
if (msg?.success) invalidate();
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
create: (payload: SubBalancerFormValues) => createMut.mutateAsync(payload),
|
||||
update: (id: number, payload: SubBalancerFormValues) => updateMut.mutateAsync({ id, payload }),
|
||||
remove: (id: number) => removeMut.mutateAsync(id),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { HttpUtil } from '@/utils';
|
||||
import { parseMsg } from '@/utils/zodValidate';
|
||||
import { keys } from '@/api/queryKeys';
|
||||
import { SubBalancerListSchema, type SubBalancer } from '@/schemas/subBalancer';
|
||||
|
||||
async function fetchSubBalancers(): Promise<SubBalancer[]> {
|
||||
const msg = await HttpUtil.get('/panel/api/sub-balancers', undefined, { silent: true });
|
||||
if (!msg?.success) throw new Error(msg?.msg || 'Failed to fetch subscription balancers');
|
||||
const validated = parseMsg(msg, SubBalancerListSchema, 'sub-balancers');
|
||||
return Array.isArray(validated.obj) ? validated.obj : [];
|
||||
}
|
||||
|
||||
export function useSubBalancersQuery() {
|
||||
const query = useQuery({
|
||||
queryKey: keys.subBalancers.list(),
|
||||
queryFn: fetchSubBalancers,
|
||||
});
|
||||
|
||||
const balancers = useMemo(() => query.data ?? [], [query.data]);
|
||||
|
||||
return {
|
||||
balancers,
|
||||
loading: query.isFetching,
|
||||
fetched: query.data !== undefined || query.isError,
|
||||
fetchError: query.error ? (query.error as Error).message : '',
|
||||
refetch: query.refetch,
|
||||
};
|
||||
}
|
||||
@@ -13,6 +13,10 @@ export const keys = {
|
||||
byInbound: (inboundId: number) => ['hosts', 'byInbound', inboundId] as const,
|
||||
tags: () => ['hosts', 'tags'] as const,
|
||||
},
|
||||
subBalancers: {
|
||||
root: () => ['sub-balancers'] as const,
|
||||
list: () => ['sub-balancers', 'list'] as const,
|
||||
},
|
||||
settings: {
|
||||
root: () => ['settings'] as const,
|
||||
all: () => ['settings', 'all'] as const,
|
||||
@@ -41,7 +45,8 @@ export const keys = {
|
||||
geodata: {
|
||||
root: () => ['xray', 'geodata'] as const,
|
||||
files: () => ['xray', 'geodata', 'files'] as const,
|
||||
categories: (file: string, query: string) => ['xray', 'geodata', 'categories', file, query] as const,
|
||||
categories: (file: string, query: string) =>
|
||||
['xray', 'geodata', 'categories', file, query] as const,
|
||||
entries: (file: string, code: string, query: string, offset: number, limit: number) =>
|
||||
['xray', 'geodata', 'entries', file, code, query, offset, limit] as const,
|
||||
},
|
||||
|
||||
@@ -35,7 +35,10 @@ export class WebSocketClient {
|
||||
}
|
||||
|
||||
connect(): void {
|
||||
if (this.ws && (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING)) {
|
||||
if (
|
||||
this.ws &&
|
||||
(this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
this.shouldReconnect = true;
|
||||
@@ -48,7 +51,9 @@ export class WebSocketClient {
|
||||
this.#cancelReconnect();
|
||||
this.reconnectAttempts = 0;
|
||||
if (this.ws) {
|
||||
try { this.ws.close(1000, 'client disconnect'); } catch {}
|
||||
try {
|
||||
this.ws.close(1000, 'client disconnect');
|
||||
} catch {}
|
||||
this.ws = null;
|
||||
}
|
||||
this.isConnected = false;
|
||||
@@ -130,7 +135,9 @@ export class WebSocketClient {
|
||||
const byteLen = new Blob([data]).size;
|
||||
if (byteLen > WebSocketClient.#MAX_PAYLOAD_BYTES) {
|
||||
console.error(`WebSocket: payload too large (${byteLen} bytes), closing`);
|
||||
try { this.ws?.close(1009, 'message too big'); } catch {}
|
||||
try {
|
||||
this.ws?.close(1009, 'message too big');
|
||||
} catch {}
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -141,7 +148,11 @@ export class WebSocketClient {
|
||||
console.error('WebSocket: invalid JSON message', err);
|
||||
return;
|
||||
}
|
||||
if (!message || typeof message !== 'object' || typeof (message as { type?: unknown }).type !== 'string') {
|
||||
if (
|
||||
!message ||
|
||||
typeof message !== 'object' ||
|
||||
typeof (message as { type?: unknown }).type !== 'string'
|
||||
) {
|
||||
console.error('WebSocket: malformed message envelope');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,10 @@ type ClientCardCommentProps = {
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export default function ClientCardComment({ comment, className = 'client-card-comment' }: ClientCardCommentProps) {
|
||||
export default function ClientCardComment({
|
||||
comment,
|
||||
className = 'client-card-comment',
|
||||
}: ClientCardCommentProps) {
|
||||
if (!comment) return null;
|
||||
|
||||
return (
|
||||
@@ -11,4 +14,4 @@ export default function ClientCardComment({ comment, className = 'client-card-co
|
||||
{comment}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
import { Button, Modal, Popconfirm, Tag, Typography } from 'antd';
|
||||
import { DeleteOutlined, ReloadOutlined } from '@ant-design/icons';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import type { ClientHwidInfo } from '@/lib/clients/hwid-log';
|
||||
|
||||
interface ClientHwidListModalProps {
|
||||
open: boolean;
|
||||
email?: string;
|
||||
zIndex?: number;
|
||||
hwids: ClientHwidInfo[];
|
||||
loading: boolean;
|
||||
clearing: boolean;
|
||||
deletingId: number | null;
|
||||
formatDate: (ts: number) => string;
|
||||
onRefresh: () => void;
|
||||
onClearAll: () => void;
|
||||
onDelete: (id: number) => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
// The single place the HWID device list is rendered — the edit form and the
|
||||
// info card share it so date format and row layout can't drift apart again.
|
||||
export default function ClientHwidListModal({
|
||||
open,
|
||||
email,
|
||||
zIndex,
|
||||
hwids,
|
||||
loading,
|
||||
clearing,
|
||||
deletingId,
|
||||
formatDate,
|
||||
onRefresh,
|
||||
onClearAll,
|
||||
onDelete,
|
||||
onClose,
|
||||
}: ClientHwidListModalProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={open}
|
||||
title={`${t('pages.clients.hwidLog')}${email ? ` — ${email}` : ''}`}
|
||||
width={520}
|
||||
zIndex={zIndex}
|
||||
onCancel={onClose}
|
||||
footer={[
|
||||
<Button key="refresh" icon={<ReloadOutlined />} loading={loading} onClick={onRefresh}>
|
||||
{t('refresh')}
|
||||
</Button>,
|
||||
<Popconfirm
|
||||
key="clear"
|
||||
title={t('pages.clients.clearHwidsConfirm')}
|
||||
onConfirm={onClearAll}
|
||||
okType="danger"
|
||||
okText={t('delete')}
|
||||
cancelText={t('cancel')}
|
||||
>
|
||||
<Button danger loading={clearing} disabled={hwids.length === 0}>
|
||||
{t('pages.clients.clearAll')}
|
||||
</Button>
|
||||
</Popconfirm>,
|
||||
<Button key="close" type="primary" onClick={onClose}>
|
||||
{t('close')}
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
{hwids.length > 0 ? (
|
||||
<div style={{ maxHeight: 360, overflowY: 'auto' }}>
|
||||
{hwids.map((entry) => (
|
||||
<div
|
||||
key={entry.id}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'flex-start',
|
||||
gap: 8,
|
||||
borderBottom: '1px solid var(--ant-color-border-secondary)',
|
||||
padding: '8px 0',
|
||||
}}
|
||||
>
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<Typography.Text strong>
|
||||
{entry.deviceModel || entry.userAgent || t('pages.clients.hwidDevice')}
|
||||
</Typography.Text>
|
||||
<br />
|
||||
<Typography.Text type="secondary">
|
||||
{[entry.deviceOs, entry.osVersion].filter(Boolean).join(' ')}
|
||||
</Typography.Text>
|
||||
<br />
|
||||
<Typography.Text type="secondary">
|
||||
{t('pages.clients.firstSeen')}: {formatDate(entry.firstSeen)}
|
||||
</Typography.Text>
|
||||
<br />
|
||||
<Typography.Text type="secondary">
|
||||
{t('pages.clients.lastSeen')}: {formatDate(entry.lastSeen)}
|
||||
</Typography.Text>
|
||||
{entry.userAgent && (
|
||||
<>
|
||||
<br />
|
||||
<Typography.Text type="secondary" style={{ wordBreak: 'break-all' }}>
|
||||
{entry.userAgent}
|
||||
</Typography.Text>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<Popconfirm
|
||||
title={t('pages.clients.deleteHwidConfirm')}
|
||||
onConfirm={() => onDelete(entry.id)}
|
||||
okType="danger"
|
||||
okText={t('delete')}
|
||||
cancelText={t('cancel')}
|
||||
>
|
||||
<Button
|
||||
danger
|
||||
type="text"
|
||||
size="small"
|
||||
aria-label={t('pages.clients.deleteHwid')}
|
||||
icon={<DeleteOutlined />}
|
||||
loading={deletingId === entry.id}
|
||||
/>
|
||||
</Popconfirm>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<Tag>{t('pages.clients.noHwids')}</Tag>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -23,8 +23,7 @@ export function ClientSpeedTag({ speed, tableCell = false }: ClientSpeedTagProps
|
||||
style={tableCell ? SPEED_TAG_STYLE : undefined}
|
||||
>
|
||||
↑ {SizeFormatter.speedFormat(speed.up)}
|
||||
{' / '}
|
||||
↓ {SizeFormatter.speedFormat(speed.down)}
|
||||
{' / '}↓ {SizeFormatter.speedFormat(speed.down)}
|
||||
</Tag>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -31,7 +31,10 @@ const meta = {
|
||||
down: { description: 'Downloaded bytes counted against the client.' },
|
||||
total: { description: 'Traffic quota in bytes; 0 or less renders as unlimited.' },
|
||||
enabled: { description: 'Grays the bar out when the client is disabled.' },
|
||||
trafficDiff: { description: 'Headroom in bytes below the quota at which the bar shifts from green to orange.' },
|
||||
trafficDiff: {
|
||||
description:
|
||||
'Headroom in bytes below the quota at which the bar shifts from green to orange.',
|
||||
},
|
||||
compact: { description: 'Smaller bar and tighter layout for dense table rows.' },
|
||||
},
|
||||
} satisfies Meta<typeof ClientTrafficCell>;
|
||||
|
||||
@@ -60,7 +60,9 @@ const ClientTrafficCell = memo(function ClientTrafficCell({
|
||||
'client-traffic-cell',
|
||||
compact ? 'is-compact' : '',
|
||||
display.isUnlimited ? 'is-unlimited' : '',
|
||||
].filter(Boolean).join(' ');
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ');
|
||||
|
||||
return (
|
||||
<Popover content={popover} trigger={['hover', 'click']} placement="top">
|
||||
@@ -77,7 +79,11 @@ const ClientTrafficCell = memo(function ClientTrafficCell({
|
||||
/>
|
||||
<span className="client-traffic-cell-limit">
|
||||
{display.isUnlimited ? (
|
||||
<span className="client-traffic-cell-infinity" role="img" aria-label={t('subscription.unlimited')}>
|
||||
<span
|
||||
className="client-traffic-cell-infinity"
|
||||
role="img"
|
||||
aria-label={t('subscription.unlimited')}
|
||||
>
|
||||
<InfinityIcon />
|
||||
</span>
|
||||
) : (
|
||||
|
||||
@@ -17,8 +17,13 @@ const meta = {
|
||||
},
|
||||
},
|
||||
argTypes: {
|
||||
label: { description: 'Protocol/type badge shown on the panel header (e.g. `vless`, `trojan`).' },
|
||||
text: { description: 'The config or share-link text to display, copy, download, and encode as a QR code.' },
|
||||
label: {
|
||||
description: 'Protocol/type badge shown on the panel header (e.g. `vless`, `trojan`).',
|
||||
},
|
||||
text: {
|
||||
description:
|
||||
'The config or share-link text to display, copy, download, and encode as a QR code.',
|
||||
},
|
||||
fileName: { description: 'File name used when downloading the text.' },
|
||||
qrRemark: { description: 'Optional remark embedded in the QR panel; falls back to `label`.' },
|
||||
showQr: { description: 'Whether to show the QR-code action button.' },
|
||||
@@ -31,8 +36,9 @@ export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
const sampleLink = 'vless://11112222-3333-4444-5555-666677778888@panel.example.com:443'
|
||||
+ '?type=ws&security=tls&path=%2Fpath#example-node';
|
||||
const sampleLink =
|
||||
'vless://11112222-3333-4444-5555-666677778888@panel.example.com:443' +
|
||||
'?type=ws&security=tls&path=%2Fpath#example-node';
|
||||
|
||||
export const Collapsed: Story = {
|
||||
args: { label: 'vless', text: sampleLink, fileName: 'client-config.txt' },
|
||||
@@ -58,5 +64,11 @@ export const Expanded: Story = {
|
||||
};
|
||||
|
||||
export const WithoutQr: Story = {
|
||||
args: { label: 'trojan', text: sampleLink, fileName: 'client-config.txt', showQr: false, tagColor: 'geekblue' },
|
||||
args: {
|
||||
label: 'trojan',
|
||||
text: sampleLink,
|
||||
fileName: 'client-config.txt',
|
||||
showQr: false,
|
||||
tagColor: 'geekblue',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -70,12 +70,18 @@ export default function ConfigBlock({
|
||||
className="config-block"
|
||||
collapsible="header"
|
||||
defaultActiveKey={defaultOpen ? ['cfg'] : []}
|
||||
items={[{
|
||||
key: 'cfg',
|
||||
label: <Tag color={tagColor} style={{ margin: 0, fontWeight: 600, letterSpacing: '0.3px' }}>{label}</Tag>,
|
||||
extra: actions,
|
||||
children: <code className="config-block-text">{text}</code>,
|
||||
}]}
|
||||
items={[
|
||||
{
|
||||
key: 'cfg',
|
||||
label: (
|
||||
<Tag color={tagColor} style={{ margin: 0, fontWeight: 600, letterSpacing: '0.3px' }}>
|
||||
{label}
|
||||
</Tag>
|
||||
),
|
||||
extra: actions,
|
||||
children: <code className="config-block-text">{text}</code>,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -39,7 +39,9 @@ function InputDemo() {
|
||||
const [value, setValue] = useState('');
|
||||
return (
|
||||
<>
|
||||
<Button type="primary" onClick={() => setOpen(true)}>Rename client</Button>
|
||||
<Button type="primary" onClick={() => setOpen(true)}>
|
||||
Rename client
|
||||
</Button>
|
||||
<div style={{ marginTop: 12 }}>Last confirmed: {value || '—'}</div>
|
||||
<PromptModal
|
||||
open={open}
|
||||
|
||||
@@ -33,15 +33,21 @@ export default function PromptModal({
|
||||
const textareaRef = useRef<HTMLTextAreaElement | null>(null);
|
||||
const inputRef = useRef<InputRef | null>(null);
|
||||
|
||||
const [openedWith, setOpenedWith] = useState<string | null>(null);
|
||||
const openKey = open ? `${type}\u0000${initialValue}` : null;
|
||||
if (openKey !== openedWith) {
|
||||
setOpenedWith(openKey);
|
||||
if (open) setValue(initialValue);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setValue(initialValue);
|
||||
setTimeout(() => {
|
||||
if (type === 'textarea') textareaRef.current?.focus();
|
||||
else inputRef.current?.focus();
|
||||
}, 50);
|
||||
}
|
||||
}, [open, initialValue, type]);
|
||||
if (!open) return;
|
||||
const id = setTimeout(() => {
|
||||
if (type === 'textarea') textareaRef.current?.focus();
|
||||
else inputRef.current?.focus();
|
||||
}, 50);
|
||||
return () => clearTimeout(id);
|
||||
}, [open, type]);
|
||||
|
||||
function onKeydown(e: React.KeyboardEvent<HTMLTextAreaElement | HTMLInputElement>) {
|
||||
if (type !== 'textarea' && e.key === 'Enter') {
|
||||
@@ -71,7 +77,11 @@ export default function PromptModal({
|
||||
<JsonEditor value={value} onChange={setValue} minHeight="240px" maxHeight="60vh" />
|
||||
) : type === 'textarea' ? (
|
||||
<Input.TextArea
|
||||
ref={(el) => { textareaRef.current = (el as unknown as { resizableTextArea?: { textArea: HTMLTextAreaElement } })?.resizableTextArea?.textArea ?? null; }}
|
||||
ref={(el) => {
|
||||
textareaRef.current =
|
||||
(el as unknown as { resizableTextArea?: { textArea: HTMLTextAreaElement } })
|
||||
?.resizableTextArea?.textArea ?? null;
|
||||
}}
|
||||
aria-label={title}
|
||||
value={value}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user