mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-11-14 00:55:01 +03:00
Compare commits
141 Commits
af5a8e5d40
..
v3.7.0
| Author | SHA1 | Date | |
|---|---|---|---|
| f727d04f65 | |||
| 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 | |||
| 380aff4d82 | |||
| 3a2f9b48da | |||
| 3f1dd4bf5a | |||
| abd320994a | |||
| 708a69acde | |||
| f75ea08ab4 | |||
| 5c7ca5b579 | |||
| b8903fadf4 | |||
| 1872659d83 | |||
| 6638ac4a1e | |||
| d6472740dc | |||
| 6e80a468e3 | |||
| e940f30bb8 | |||
| 6a674c7f0c | |||
| 81cfd8570e | |||
| 5c9268c431 | |||
| 2b1fe1fd02 | |||
| dc1979a14c | |||
| 8cec47a8a5 | |||
| 4b0e9f9b60 | |||
| 5d6d98d1f9 | |||
| b53a5515d6 | |||
| 3fa88adbd7 | |||
| 930a0ed59d | |||
| f22df49a71 | |||
| b4e4478699 | |||
| bab39393f1 | |||
| 338822ab07 | |||
| 43bc915397 | |||
| dafd3c0e64 | |||
| acbf09e710 | |||
| be70535b94 | |||
| 2d669fa4b7 | |||
| 8c8556ab32 | |||
| 03950b1295 | |||
| d7698ec7aa | |||
| 7c8a9a6909 | |||
| 694ad6deae | |||
| 1793a9b8b4 | |||
| 8e7fb144ee | |||
| 0f14ce7551 | |||
| 7ecd88b9e3 | |||
| 1230559e69 | |||
| aecbad3ab1 | |||
| 2217213e9f | |||
| ad32144c42 | |||
| 34c248bb79 | |||
| 17fea2f656 | |||
| c5dec64d36 | |||
| b56b087254 | |||
| 3bb87e80aa | |||
| 60453bf523 | |||
| bb29b6afec | |||
| 3b19091547 | |||
| 0496c23a26 | |||
| 1396005082 | |||
| b70c5abce8 | |||
| 20b3f84f77 | |||
| d291e1c5ee | |||
| ecadfd0e60 | |||
| d05e44e401 | |||
| 9165ab67eb | |||
| 0a30a03cb7 | |||
| 286a93474d | |||
| 238e4bb314 | |||
| 4a5f6771b3 | |||
| 64f4f0746c | |||
| 79ef85b59f | |||
| 5b80d4562d | |||
| e2f75acad2 | |||
| 69a8237581 | |||
| f3f57e66f5 | |||
| 8a8da88548 | |||
| 1f846c3cb2 | |||
| 1c255fc00c | |||
| 75032fd498 | |||
| ece1655939 | |||
| cb902314db | |||
| 7eacce6a46 | |||
| 199ddaf485 | |||
| d142307366 | |||
| 3883882726 | |||
| 216d18b3c4 | |||
| 2a8c3bc0db | |||
| e71b75e99e | |||
| 5bc81dfd1d | |||
| f4b7b08e08 | |||
| 1ff90c5b66 | |||
| 138e1bd840 | |||
| 31c1eed5dc | |||
| 264f61eb90 | |||
| ac584cfc90 | |||
| 91c5d7b19f | |||
| b2fe233108 | |||
| 5373786faa | |||
| c377dca27c | |||
| c56f6447a8 | |||
| 66740b7ef4 | |||
| 8d02ae28f5 | |||
| 2c943da3e0 | |||
| f52c3c4837 | |||
| 1e2d6f6081 |
@@ -1,7 +1,7 @@
|
||||
name: Bug report
|
||||
description: Report something that is broken or behaving unexpectedly
|
||||
title: "[Bug]: "
|
||||
labels: ["bug", "needs triage"]
|
||||
labels: ["bug"]
|
||||
|
||||
body:
|
||||
- type: markdown
|
||||
@@ -64,7 +64,10 @@ body:
|
||||
id: screenshots
|
||||
attributes:
|
||||
label: Screenshots
|
||||
description: Drag images directly into this field. Redact any sensitive data.
|
||||
description: |
|
||||
Drag images directly into this field. Redact any sensitive data.
|
||||
Images cannot be searched or machine-read — always paste the exact
|
||||
error text or log lines as text in the fields above as well.
|
||||
validations:
|
||||
required: false
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
name: Feature request
|
||||
description: Suggest an idea or improvement for 3x-ui
|
||||
title: "[Feature]: "
|
||||
labels: ["enhancement", "needs triage"]
|
||||
labels: ["enhancement"]
|
||||
|
||||
body:
|
||||
- type: markdown
|
||||
|
||||
@@ -73,7 +73,10 @@ body:
|
||||
id: screenshots
|
||||
attributes:
|
||||
label: Screenshots or config snippets
|
||||
description: Drag images or paste relevant config. Redact tokens, real domains, client UUIDs.
|
||||
description: |
|
||||
Drag images or paste relevant config. Redact tokens, real domains,
|
||||
client UUIDs. Prefer pasted text over screenshots — images cannot
|
||||
be searched or machine-read.
|
||||
validations:
|
||||
required: false
|
||||
|
||||
|
||||
@@ -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,8 @@ on:
|
||||
- "go.sum"
|
||||
- "frontend/**"
|
||||
- ".nvmrc"
|
||||
- "Makefile"
|
||||
- ".github/workflows/ci.yml"
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
@@ -17,6 +19,8 @@ on:
|
||||
- "go.sum"
|
||||
- "frontend/**"
|
||||
- ".nvmrc"
|
||||
- "Makefile"
|
||||
- ".github/workflows/ci.yml"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -53,6 +57,9 @@ jobs:
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
env:
|
||||
XUI_DB_TYPE: postgres
|
||||
XUI_DB_DSN: "host=127.0.0.1 port=5432 user=postgres password=postgres dbname=xui_durable sslmode=disable"
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/setup-go@v7
|
||||
@@ -64,9 +71,24 @@ jobs:
|
||||
- name: PostgreSQL durable-first tests
|
||||
run: |
|
||||
set -o pipefail
|
||||
XUI_DB_TYPE=postgres XUI_DB_DSN="host=127.0.0.1 port=5432 user=postgres password=postgres dbname=xui_durable sslmode=disable" \
|
||||
go test ./internal/web/service -run 'PostgresCommitFailure' -count=1 -v | tee /tmp/postgres-durable-first.log
|
||||
if grep -q -- '--- SKIP' /tmp/postgres-durable-first.log; then
|
||||
go test ./internal/web/service -run 'PostgresCommitFailure' -count=1 -v | tee /tmp/postgres-durable-first.log
|
||||
# Count passes rather than assert no SKIP: a renamed or deleted test
|
||||
# prints "no tests to run" and exits 0, leaving the step green for nothing.
|
||||
passed=$(grep -c -- '--- PASS' /tmp/postgres-durable-first.log || true)
|
||||
if [ "$passed" -lt 1 ]; then
|
||||
echo "expected at least 1 passing durable-first test, got $passed" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: PostgreSQL schema and migration tests
|
||||
run: |
|
||||
set -o pipefail
|
||||
go test ./internal/database -run '^(TestHostAutoMigrateCreatesColumns_Postgres|TestMigrate_Postgres)$' -count=1 -v | tee /tmp/postgres-schema.log
|
||||
# Both must pass. Counting, not SKIP-matching: renaming either test would
|
||||
# otherwise leave this step green while testing nothing.
|
||||
passed=$(grep -c -- '--- PASS' /tmp/postgres-schema.log || true)
|
||||
if [ "$passed" -lt 2 ]; then
|
||||
echo "expected 2 passing PostgreSQL schema tests, got $passed" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -162,9 +184,15 @@ jobs:
|
||||
- name: Install
|
||||
run: npm ci
|
||||
working-directory: frontend
|
||||
- name: Verify generated MSW worker is current
|
||||
run: git diff --exit-code -- public/mockServiceWorker.js package-lock.json
|
||||
working-directory: frontend
|
||||
- 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
|
||||
|
||||
+568
-582
File diff suppressed because it is too large
Load Diff
@@ -55,6 +55,8 @@ jobs:
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
provenance: mode=max
|
||||
sbom: true
|
||||
platforms: linux/amd64,linux/arm64/v8,linux/arm/v7,linux/arm/v6,linux/386
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
@@ -42,6 +42,9 @@ jobs:
|
||||
- name: Lint
|
||||
run: pnpm lint
|
||||
|
||||
- name: Format check
|
||||
run: pnpm format:check
|
||||
|
||||
- name: Test
|
||||
run: pnpm test
|
||||
|
||||
|
||||
@@ -109,7 +109,7 @@ jobs:
|
||||
if [[ "$GITHUB_REF" != refs/tags/* ]]; then
|
||||
LDFLAGS="$LDFLAGS -X github.com/mhsanaei/3x-ui/v3/internal/config.buildCommit=${GITHUB_SHA::8} -X github.com/mhsanaei/3x-ui/v3/internal/config.buildDate=$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||
fi
|
||||
go build -ldflags "$LDFLAGS" -o xui-release -v main.go
|
||||
go build -buildvcs=true -ldflags "$LDFLAGS" -o xui-release -v .
|
||||
file xui-release
|
||||
ldd xui-release || echo "Static binary confirmed"
|
||||
|
||||
@@ -247,6 +247,7 @@ jobs:
|
||||
msystem: MINGW64
|
||||
update: true
|
||||
install: >-
|
||||
git
|
||||
mingw-w64-x86_64-gcc
|
||||
mingw-w64-x86_64-sqlite3
|
||||
mingw-w64-x86_64-pkg-config
|
||||
@@ -270,7 +271,7 @@ jobs:
|
||||
if [[ "$GITHUB_REF" != refs/tags/* ]]; then
|
||||
LDFLAGS="$LDFLAGS -X github.com/mhsanaei/3x-ui/v3/internal/config.buildCommit=${GITHUB_SHA:0:8} -X github.com/mhsanaei/3x-ui/v3/internal/config.buildDate=$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||
fi
|
||||
go build -ldflags "$LDFLAGS" -o xui-release.exe -v main.go
|
||||
go build -buildvcs=true -ldflags "$LDFLAGS" -o xui-release.exe -v .
|
||||
|
||||
- name: Copy and download resources
|
||||
shell: pwsh
|
||||
|
||||
@@ -19,9 +19,6 @@ backup/
|
||||
bin/
|
||||
x-ui/
|
||||
dist/
|
||||
!internal/web/dist/
|
||||
internal/web/dist/*
|
||||
!internal/web/dist/.gitkeep
|
||||
release/
|
||||
node_modules/
|
||||
|
||||
|
||||
+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:
|
||||
|
||||
Vendored
+12
-3
@@ -29,10 +29,19 @@
|
||||
"XUI_LOG_FOLDER": "x-ui",
|
||||
"XUI_BIN_FOLDER": "x-ui",
|
||||
"XUI_DB_TYPE": "postgres",
|
||||
"XUI_DB_DSN": "postgres://xui:xuipass@127.0.0.1:5432/xui?sslmode=disable",
|
||||
"PATH": "C:\\Program Files\\PostgreSQL\\18\\bin;${env:PATH}"
|
||||
"XUI_DB_DSN": "postgres://xui:xuipass@127.0.0.1:5432/xui?sslmode=disable"
|
||||
},
|
||||
"windows": {
|
||||
"env": {
|
||||
"XUI_DEBUG": "true",
|
||||
"XUI_LOG_FOLDER": "x-ui",
|
||||
"XUI_BIN_FOLDER": "x-ui",
|
||||
"XUI_DB_TYPE": "postgres",
|
||||
"XUI_DB_DSN": "postgres://xui:xuipass@127.0.0.1:5432/xui?sslmode=disable",
|
||||
"PATH": "C:\\Program Files\\PostgreSQL\\18\\bin;${env:PATH}"
|
||||
}
|
||||
},
|
||||
"console": "integratedTerminal"
|
||||
},
|
||||
}
|
||||
]
|
||||
}
|
||||
Vendored
+9
-1
@@ -8,9 +8,17 @@
|
||||
"args": [
|
||||
"build",
|
||||
"-o",
|
||||
"bin/3x-ui.exe",
|
||||
"bin/3x-ui",
|
||||
"./main.go"
|
||||
],
|
||||
"windows": {
|
||||
"args": [
|
||||
"build",
|
||||
"-o",
|
||||
"bin/3x-ui.exe",
|
||||
"./main.go"
|
||||
]
|
||||
},
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}"
|
||||
},
|
||||
|
||||
@@ -8,12 +8,14 @@ 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
|
||||
(`github.com/mhsanaei/mtg-multi`, a multi-secret fork built from source;
|
||||
`internal/mtproto/`) — outside Xray, one process per inbound serving each
|
||||
(a multi-secret mtg fork — NOT a Go dependency; its prebuilt release binary is
|
||||
fetched at image/release build time by `DockerInit.sh` + `release.yml`,
|
||||
panel-side code in `internal/mtproto/`) — outside Xray, one process per inbound
|
||||
serving each
|
||||
client's FakeTLS secret via the fork's `[secrets]` section (plus per-client
|
||||
ad-tags via `[secret-ad-tags]` and per-client data quota / expiry via
|
||||
`[secret-limits]`, mapped from the client's `totalGB`/`expiryTime`). Client,
|
||||
@@ -32,11 +34,21 @@ file locations when it can answer in one hop.
|
||||
- `main.go` — entry point + `x-ui` CLI (run, migrate, migrate-db, setting, cert).
|
||||
- `internal/config/` — env parsing (XUI_DEBUG, XUI_LOG_LEVEL, XUI_LOG_FOLDER,
|
||||
XUI_BIN_FOLDER, XUI_SKIP_HSTS, XUI_PORT, XUI_DB_*).
|
||||
- `internal/database/` + `internal/database/model/` — GORM schema (Inbound,
|
||||
Client, Setting, User), inbound Protocol enum, AutoMigrate + hand-written
|
||||
migrations in `db.go`.
|
||||
- `internal/database/` + `internal/database/model/` — GORM schema (~24 models;
|
||||
Inbound, Client, Setting, User are the core), inbound Protocol enum,
|
||||
AutoMigrate + hand-written migrations in `db.go`.
|
||||
- `internal/xray/` — Xray child-process lifecycle, config generation, gRPC API.
|
||||
- `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).
|
||||
@@ -46,7 +58,8 @@ 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/` — 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/`.
|
||||
- `locale/` + `translation/` — i18n, 13 embedded locale JSON files.
|
||||
@@ -55,26 +68,49 @@ file locations when it can answer in one hop.
|
||||
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`.
|
||||
- `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
|
||||
share-link or install-command output changes.
|
||||
|
||||
## Hard rules (non-negotiable)
|
||||
- NO `//` line comments in committed Go/TS. Names carry meaning; rename instead
|
||||
of annotating. Exempt: `//go:build`, `//go:generate`, and other directives.
|
||||
- Fix size must match bug size. Find the root cause, then make the SMALLEST
|
||||
change that removes it — a one-line guard beats a new subsystem. A small bug
|
||||
does not earn new columns, jobs, abstractions, config knobs or helper layers.
|
||||
If a fix genuinely needs new architecture, say so and get agreement first;
|
||||
never ship it unasked next to the fix.
|
||||
- Comments in committed Go/TS: 2 lines MAX per comment block. Make the name
|
||||
carry the meaning first and rename rather than annotate; spend the 2 lines on
|
||||
the *why* a name cannot hold — an invariant, an issue number, a non-obvious
|
||||
constraint. Exempt: `//go:build`, `//go:generate`, and other directives.
|
||||
HTML `<!-- -->` is fine. (A linter cannot enforce this — you must.)
|
||||
- New `g.POST`/`g.GET` in `internal/web/controller/` REQUIRES a matching entry
|
||||
in `frontend/src/pages/api-docs/endpoints.ts`, then `make gen` (or
|
||||
`cd frontend && npm run gen`). It is a hand-maintained registry — nothing checks
|
||||
it against the Go routes, so an omitted route silently vanishes from the docs.
|
||||
`cd frontend && npm run gen`). Hand-maintained but pinned both ways by
|
||||
`TestRouteRegistryContract` (`internal/web/routes_contract_test.go`): a missing
|
||||
OR stale entry fails `make test-go`. Scope: `/panel/api/*` + a few session
|
||||
routes; sub-server routes are exempt.
|
||||
- Response examples come from Go struct `example:` tags via `tools/openapigen` —
|
||||
never hand-write them. A new struct must be added to openapigen's `StructAllow`
|
||||
allowlist (`tools/openapigen/main.go`) or it is silently omitted from
|
||||
schemas/examples (and `build-openapi.mjs` then fails on the missing schema).
|
||||
- A new English i18n key must be added to EVERY locale JSON in
|
||||
`internal/web/translation/` (13 files). Missing keys fall back to en-US (or
|
||||
render the raw key if absent there too); nothing fails the build, so they are
|
||||
easy to miss.
|
||||
- A new or renamed endpoint has a FOURTH step nothing checks: copy
|
||||
`frontend/public/openapi.json` → `docs/public/openapi.json`, then
|
||||
`cd docs && pnpm gen:api` to refresh the MDX under
|
||||
`docs/content/docs/en/reference/api/`. `docs-ci.yml` fires only on `docs/**`.
|
||||
- A new English i18n key goes in EVERY locale JSON in `internal/web/translation/`
|
||||
(13 files) AND must be referenced from `frontend/src` or Go in the SAME commit —
|
||||
`frontend/src/test/i18n-dead-keys.test.ts` fails both ways. It is a frontend
|
||||
test, so run `npm test`, not just `make test-go`. At runtime the frontend falls
|
||||
back to en-US; Go (`internal/web/locale/`) returns "" for an unknown key.
|
||||
- DB / model changes require a migration in `internal/database/db.go`.
|
||||
- Conventional-commit prefixes (`feat`, `fix`, `refactor`, `chore`, `docs`,
|
||||
`style`): `<area>: short imperative summary`, then a body explaining the why.
|
||||
- Every state-changing inbound/client op dispatches through `runtime.Runtime`
|
||||
(`internal/web/runtime/`) — never straight to `internal/xray/api.go`, never from
|
||||
a controller or cron job. A direct call passes every local test and silently
|
||||
breaks every multi-node deployment. Other layering rules: `docs/architecture.md` §8.
|
||||
- Conventional commits: `type(area): short imperative summary`, then a body
|
||||
explaining the why. Types in use: `fix`, `feat`, `chore`, `refactor`, `perf`,
|
||||
`docs`, `style`.
|
||||
|
||||
## Go conventions
|
||||
- Stdlib `testing` only (no testify). Table-driven, `t.Run` subtests,
|
||||
@@ -83,13 +119,26 @@ file locations when it can answer in one hop.
|
||||
`database.InitDB(filepath.Join(t.TempDir(), "x-ui.db"))` +
|
||||
`t.Cleanup(func() { _ = database.CloseDB() })`; `httptest` for HTTP.
|
||||
`internal/sub`'s `initSubDB(t)` is the template.
|
||||
- A test must fail without its fix. Write it, revert the fix, watch it go red,
|
||||
restore. A test that passes either way is worse than no test: it certifies
|
||||
nothing and then gets cited as proof the fix works.
|
||||
- Test what can actually break. No test for a getter, a constant, a rename, a
|
||||
pure map lookup, or inputs the function can never receive. One real test that
|
||||
drives the bug through the actual code path beats five that restate the code.
|
||||
- Code must pass `golangci-lint run` (gofumpt + goimports formatting): `make lint`.
|
||||
- Postgres, xray-gRPC-e2e and scale tests `t.Skip` unless `XUI_TEST_PG_DSN`,
|
||||
`XUI_DB_TYPE`+`XUI_DB_DSN`, `XRAY_E2E_BINARY` or `XUI_SCALE_TEST` is set — a
|
||||
green `go test ./...` does not mean those paths ran.
|
||||
|
||||
## Frontend conventions (summary; full version in frontend/CLAUDE.md)
|
||||
- Ant Design 6 only — no Tailwind/shadcn. Targeted tweaks, not rewrites.
|
||||
- TS strict; `@typescript-eslint/no-explicit-any` is an error. Zod schemas in
|
||||
`src/schemas/` are the source of truth; infer types with `z.infer`, never
|
||||
hand-write. Do not edit `src/generated/`.
|
||||
- Node 24 (`.nvmrc`) — `make gen` imports `.ts` directly and needs its type
|
||||
stripping; Node 22 dies with `ERR_UNKNOWN_FILE_EXTENSION`. `npm test` includes
|
||||
a headless-Chromium Storybook project, so run
|
||||
`npx playwright install --with-deps chromium` once or `make verify` fails.
|
||||
- Editing `frontend/src` does NOT change what users see until the Vite build is
|
||||
regenerated into `internal/web/dist/`. In `XUI_DEBUG=true`, HTML is served from
|
||||
the frozen embedded FS but JS/CSS off disk — after `npm run build` you MUST
|
||||
@@ -99,15 +148,23 @@ file locations when it can answer in one hop.
|
||||
output changes, never to make a red test green.
|
||||
|
||||
## Build, test, verify
|
||||
Run `make help` for all targets. The full local gate that mirrors CI:
|
||||
A fresh clone has no `internal/web/dist/`, so a bare `go build ./...` dies with
|
||||
`pattern all:dist: no matching files found` while ~35 other packages pass — it
|
||||
reads as a broken repo, not a missing step. Run `make dist-stub` once; every
|
||||
`make` Go target already depends on it, which is why `make test-go` beats
|
||||
`go test ./...`. Run `make help` for all targets. The local gate:
|
||||
|
||||
make verify
|
||||
make verify # gen-check + lint + typecheck + test + build + build-storybook
|
||||
|
||||
That is the *fast* gate, not all of CI. `ci.yml` also runs `make race`,
|
||||
`make vulncheck`, a live-Postgres job (where a SKIP counts as a failure) and a
|
||||
30s fuzz smoke on `FuzzParseLink`/`FuzzDecodeCertPin` — run those locally when
|
||||
you touch DB/dialect or parser code.
|
||||
|
||||
Common targets: `make gen` (regenerate Zod/OpenAPI), `make lint` (Go + frontend),
|
||||
`make test` (Go `-shuffle=on` + frontend), `make race`, `make build`. See `Makefile`.
|
||||
|
||||
## Definition of done (before opening a PR)
|
||||
1. `make gen` and confirm `git diff` on `frontend/src/generated` +
|
||||
`frontend/public/openapi.json` is clean.
|
||||
2. `make verify` passes.
|
||||
3. Diff is focused; refactors are separate from feature work.
|
||||
1. `make verify` passes — its `gen-check` already runs `make gen` and fails on a
|
||||
dirty `frontend/src/generated` / `frontend/public/openapi.json`.
|
||||
2. Diff is focused; refactors are separate from feature work.
|
||||
|
||||
+7
-6
@@ -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.
|
||||
@@ -184,9 +184,9 @@ Only a genuinely **standalone bundle** (like `login` or `subpage`, reachable wit
|
||||
- **TypeScript strict mode** — all new code in `.ts` / `.tsx`. Run `npm run typecheck` (`tsc --noEmit`) before pushing. The path alias `@/*` resolves to `src/*`.
|
||||
- **Ant Design 6** is the only UI kit — no Tailwind, no shadcn. A previous attempt to migrate was rolled back. Small, targeted UX tweaks beat sweeping rewrites; raise broader visual changes for discussion before implementing.
|
||||
- **Function components + hooks** everywhere. No class components.
|
||||
- **No `//` line comments** in committed JS/TS/Vue/Go. HTML `<!-- ... -->` is fine for template structure. Names should carry the meaning; rename rather than annotate. Comments are reserved for the *why*, and only when the reason is surprising.
|
||||
- **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,16 +31,24 @@ 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
|
||||
|
||||
.PHONY: msw-worker-check
|
||||
msw-worker-check: ## Verify the tracked worker matches the installed MSW runtime
|
||||
cmp $(FRONTEND)/public/mockServiceWorker.js $(FRONTEND)/node_modules/msw/lib/mockServiceWorker.js
|
||||
|
||||
.PHONY: test-go
|
||||
test-go: dist-stub ## Go tests (shuffle, no cache)
|
||||
go test -shuffle=on -count=1 $(GO_PKGS)
|
||||
@@ -72,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 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.
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
# Security Policy
|
||||
|
||||
## Reporting a vulnerability
|
||||
|
||||
Do not open a public issue for anything you believe is exploitable — an
|
||||
authentication bypass, remote code execution, injection, secret or
|
||||
credential exposure, privilege escalation. A public report gives attackers
|
||||
a head start against every 3x-ui deployment.
|
||||
|
||||
Instead, use GitHub's private vulnerability reporting: open this
|
||||
repository's **Security** tab and click **Report a vulnerability**. Include
|
||||
the affected 3x-ui version, reproduction steps, and the impact you see.
|
||||
You will receive replies in the advisory thread.
|
||||
|
||||
There is no bug-bounty program. Fixes ship in the next release, and the
|
||||
advisory is published after a fixed version is available.
|
||||
|
||||
## Supported versions
|
||||
|
||||
Only the latest release receives security fixes. Update with the install
|
||||
script or your package channel and confirm the problem still exists before
|
||||
reporting.
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -87,6 +87,24 @@ docker run --rm \
|
||||
*) echo "FAIL: panel did not serve (status ${code:-none})"; tail -n 30 /tmp/xui.log; exit 1 ;;
|
||||
esac
|
||||
|
||||
echo "--- verifying a second install preserves custom bin/ files ---"
|
||||
echo "custom-sentinel" > /usr/local/x-ui/bin/geoip_custom.dat
|
||||
geoip_sum_before=$(sha256sum /usr/local/x-ui/bin/geoip.dat | cut -d" " -f1)
|
||||
|
||||
if [ -n "${XUI_SMOKE_VERSION:-}" ]; then
|
||||
cat /root/install.sh | bash -s -- "$XUI_SMOKE_VERSION"
|
||||
else
|
||||
cat /root/install.sh | bash
|
||||
fi
|
||||
|
||||
test -f /usr/local/x-ui/bin/geoip_custom.dat \
|
||||
|| { echo "FAIL: custom bin/ file did not survive a second install"; exit 1; }
|
||||
[ "$(cat /usr/local/x-ui/bin/geoip_custom.dat)" = "custom-sentinel" ] \
|
||||
|| { echo "FAIL: custom bin/ file content changed across a second install"; exit 1; }
|
||||
geoip_sum_after=$(sha256sum /usr/local/x-ui/bin/geoip.dat | cut -d" " -f1)
|
||||
[ "$geoip_sum_after" = "$geoip_sum_before" ] \
|
||||
|| { echo "FAIL: bundled geoip.dat changed across a same-version reinstall"; exit 1; }
|
||||
|
||||
echo "SMOKE_PASS: user=$XUI_USERNAME port=$XUI_PANEL_PORT path=$XUI_WEB_BASE_PATH"
|
||||
'
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -1,30 +1,18 @@
|
||||
import '../global.css';
|
||||
import { RootProvider } from 'fumadocs-ui/provider/next';
|
||||
import { Inter, Vazirmatn } from 'next/font/google';
|
||||
import { i18n, localeDirection } from '@/lib/i18n';
|
||||
import { i18n } from '@/lib/i18n';
|
||||
import { provider } from '@/lib/i18n-ui';
|
||||
import SearchDialog from '@/components/search-dialog';
|
||||
|
||||
const inter = Inter({ subsets: ['latin'], display: 'swap' });
|
||||
// Persian UI font; covers Arabic + Latin glyphs so mixed content renders well.
|
||||
const vazirmatn = Vazirmatn({ subsets: ['arabic'], display: 'swap' });
|
||||
|
||||
export function generateStaticParams() {
|
||||
return i18n.languages.map((lang) => ({ lang }));
|
||||
}
|
||||
|
||||
export default async function LangLayout({ params, children }: LayoutProps<'/[lang]'>) {
|
||||
const { lang } = await params;
|
||||
const dir = localeDirection(lang);
|
||||
const fontClassName = lang === 'fa' ? vazirmatn.className : inter.className;
|
||||
|
||||
return (
|
||||
<html lang={lang} dir={dir} className={fontClassName} suppressHydrationWarning>
|
||||
<body className="flex min-h-screen flex-col" suppressHydrationWarning>
|
||||
<RootProvider i18n={provider(lang)} search={{ SearchDialog }}>
|
||||
{children}
|
||||
</RootProvider>
|
||||
</body>
|
||||
</html>
|
||||
<RootProvider i18n={provider(lang)} search={{ SearchDialog }} theme={{ enabled: false }}>
|
||||
{children}
|
||||
</RootProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
+30
-5
@@ -1,10 +1,16 @@
|
||||
import type { Metadata } from 'next';
|
||||
import type { ReactNode } from 'react';
|
||||
import { Inter, Vazirmatn } from 'next/font/google';
|
||||
import './global.css';
|
||||
import { appName, appTagline, siteUrl } from '@/lib/shared';
|
||||
import { i18n, localeDirection } from '@/lib/i18n';
|
||||
|
||||
// Global SEO defaults. The real <html>/<body> live in `app/[lang]/layout.tsx`
|
||||
// so we can set `lang`/`dir` per locale (RTL for fa); this root layout is a
|
||||
// pass-through that only carries site-wide metadata.
|
||||
const inter = Inter({ subsets: ['latin'], display: 'swap' });
|
||||
// Persian UI font; covers Arabic + Latin glyphs so mixed content renders well.
|
||||
const vazirmatn = Vazirmatn({ subsets: ['arabic'], display: 'swap' });
|
||||
|
||||
// Global SEO defaults and document shell. Locale-aware html attributes are
|
||||
// computed from route params so RTL locales get a correct base direction.
|
||||
export const metadata: Metadata = {
|
||||
metadataBase: new URL(siteUrl),
|
||||
title: {
|
||||
@@ -26,6 +32,25 @@ export const metadata: Metadata = {
|
||||
},
|
||||
};
|
||||
|
||||
export default function RootLayout({ children }: { children: ReactNode }) {
|
||||
return children;
|
||||
export default async function RootLayout({
|
||||
children,
|
||||
params,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
params: Promise<{ lang?: string }>;
|
||||
}) {
|
||||
const { lang: rawLang } = await params;
|
||||
const lang = i18n.languages.includes(rawLang as (typeof i18n.languages)[number])
|
||||
? (rawLang as (typeof i18n.languages)[number])
|
||||
: i18n.defaultLanguage;
|
||||
const dir = localeDirection(lang);
|
||||
const fontClassName = lang === 'fa' ? vazirmatn.className : inter.className;
|
||||
|
||||
return (
|
||||
<html lang={lang} dir={dir} className={fontClassName} suppressHydrationWarning>
|
||||
<body className="flex min-h-screen flex-col" suppressHydrationWarning>
|
||||
{children}
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
||||
+111
-98
@@ -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):
|
||||
@@ -147,7 +150,9 @@ node heartbeat every 5s, periodic traffic resets (hourly/daily/weekly/monthly).
|
||||
│ │ ├── inbound.go # Inbound JSON shaping
|
||||
│ │ ├── client_traffic.go # ClientTraffic model (persisted as client_traffics)
|
||||
│ │ ├── traffic.go # Traffic type helpers
|
||||
│ │ └── log_writer.go # Pipe Xray stdout/stderr into the panel logger
|
||||
│ │ ├── log_writer.go # Pipe Xray stdout/stderr into the panel logger
|
||||
│ │ └── geodata/ # Browse geosite/geoip .dat: streaming protowire reader,
|
||||
│ │ # cached category index, routing-token parsing (token.go)
|
||||
│ │
|
||||
│ ├── web/ # The panel server
|
||||
│ │ ├── web.go # ⭐ Server bootstrap: initRouter (all routes) + startTask (all cron jobs)
|
||||
@@ -159,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)
|
||||
│ │ │ ├── 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
|
||||
@@ -189,6 +194,7 @@ node heartbeat every 5s, periodic traffic resets (hourly/daily/weekly/monthly).
|
||||
│ │ │ ├── traffic_writer.go # Batched persistence of traffic deltas to the DB
|
||||
│ │ │ ├── xray.go # ⭐ XrayService: config gen + restart/hot-apply (~1.2k lines)
|
||||
│ │ │ ├── xray_setting.go # Raw Xray config persistence
|
||||
│ │ │ ├── geodata.go # Geo database browsing + routing-token validation
|
||||
│ │ │ ├── xray_metrics.go # Xray observability metrics
|
||||
│ │ │ ├── metric_history.go # Historical system/xray metrics
|
||||
│ │ │ ├── reality_scan.go # REALITY target scanner
|
||||
@@ -197,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
|
||||
@@ -265,7 +271,7 @@ node heartbeat every 5s, periodic traffic resets (hourly/daily/weekly/monthly).
|
||||
│ │ └── queries/ # TanStack Query hooks (useNodesQuery, useStatusQuery, …)
|
||||
│ ├── schemas/ # Zod schemas: protocols, forms, api, primitives
|
||||
│ ├── generated/ # ⚠️ GENERATED from Go (see §5.5): schemas.ts, types.ts, zod.ts, examples.ts
|
||||
│ ├── components/ # Reusable UI (clients/ form/ ui/ viz/ feedback/ utility/)
|
||||
│ ├── components/ # Reusable UI (clients/ form/ geodata/ ui/ viz/ feedback/ utility/)
|
||||
│ ├── lib/ # Frontend domain logic (xray/ inbounds/ clients/)
|
||||
│ ├── hooks/, models/, layouts/, i18n/, utils/, styles/
|
||||
│ └── test/ # Vitest + golden fixtures (config-generation snapshot tests)
|
||||
@@ -309,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`,
|
||||
@@ -326,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,
|
||||
@@ -335,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`).
|
||||
@@ -357,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
|
||||
|
||||
@@ -397,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/
|
||||
@@ -435,68 +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` |
|
||||
| **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 |
|
||||
|
||||
---
|
||||
|
||||
@@ -517,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.
|
||||
@@ -531,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)
|
||||
@@ -542,12 +554,13 @@ golangci-lint run # full lint (gofumpt + goimports formatting)
|
||||
go run main.go # run the panel locally (serves embedded dist if built)
|
||||
```
|
||||
|
||||
**Frontend (`cd frontend`, Node ≥ 22):**
|
||||
**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)
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
'use client';
|
||||
|
||||
import { create } from '@orama/orama';
|
||||
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,20 +21,16 @@ 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 Orama
|
||||
// setups — with an initOrama 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,
|
||||
initOrama: () => create({ schema: { _: 'string' }, language: 'english' }),
|
||||
initDB: () => create({ schema: { _: 'string' }, language: 'english' }),
|
||||
}),
|
||||
[locale],
|
||||
);
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
'use client';
|
||||
|
||||
import { Moon, Sun } from 'lucide-react';
|
||||
import { useEffect, useState, useSyncExternalStore } from 'react';
|
||||
import type { ComponentProps } from 'react';
|
||||
import { cn } from '@/lib/cn';
|
||||
|
||||
type ThemeMode = 'light-dark' | 'light-dark-system';
|
||||
type ThemePref = 'light' | 'dark' | 'system';
|
||||
|
||||
const STORAGE_KEY = 'docs-theme';
|
||||
|
||||
// `useSyncExternalStore` supplies the same value for SSR and hydration, then
|
||||
// switches to the browser value after React has attached to the markup.
|
||||
const subscribeToHydration = () => () => {};
|
||||
const getHydrationClientSnapshot = () => true;
|
||||
const getHydrationServerSnapshot = () => false;
|
||||
|
||||
function getStoredTheme(): ThemePref {
|
||||
if (typeof window === 'undefined') return 'system';
|
||||
const raw = window.localStorage.getItem(STORAGE_KEY);
|
||||
return raw === 'light' || raw === 'dark' || raw === 'system' ? raw : 'system';
|
||||
}
|
||||
|
||||
function getResolvedTheme(theme: ThemePref): 'light' | 'dark' {
|
||||
if (theme !== 'system') return theme;
|
||||
if (typeof window === 'undefined') return 'light';
|
||||
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
||||
}
|
||||
|
||||
function applyTheme(theme: ThemePref): void {
|
||||
if (typeof document === 'undefined') return;
|
||||
const resolved = getResolvedTheme(theme);
|
||||
const root = document.documentElement;
|
||||
root.classList.toggle('dark', resolved === 'dark');
|
||||
root.style.colorScheme = resolved;
|
||||
}
|
||||
|
||||
export function DocsThemeSwitch({
|
||||
className,
|
||||
mode = 'light-dark-system',
|
||||
...props
|
||||
}: {
|
||||
className?: string;
|
||||
mode?: ThemeMode;
|
||||
} & Omit<ComponentProps<'div'>, 'children'>) {
|
||||
// Keep the server and first client render identical. Reading localStorage or
|
||||
// matchMedia here would make a persisted/system preference change the client
|
||||
// markup before React has finished hydrating it.
|
||||
const [selectedTheme, setSelectedTheme] = useState<ThemePref>('system');
|
||||
const hydrated = useSyncExternalStore(
|
||||
subscribeToHydration,
|
||||
getHydrationClientSnapshot,
|
||||
getHydrationServerSnapshot,
|
||||
);
|
||||
const theme = hydrated ? getStoredTheme() : selectedTheme;
|
||||
|
||||
useEffect(() => {
|
||||
if (hydrated) applyTheme(theme);
|
||||
}, [hydrated, theme]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!hydrated) return;
|
||||
if (theme !== 'system') return;
|
||||
const media = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
const update = () => applyTheme('system');
|
||||
media.addEventListener('change', update);
|
||||
return () => media.removeEventListener('change', update);
|
||||
}, [hydrated, theme]);
|
||||
|
||||
const resolved = hydrated ? getResolvedTheme(theme) : 'light';
|
||||
|
||||
const setTheme = (nextTheme: ThemePref) => {
|
||||
window.localStorage.setItem(STORAGE_KEY, nextTheme);
|
||||
applyTheme(nextTheme);
|
||||
setSelectedTheme(nextTheme);
|
||||
};
|
||||
|
||||
const nextTheme = () => {
|
||||
if (mode === 'light-dark') return resolved === 'dark' ? 'light' : 'dark';
|
||||
if (theme === 'light') return 'dark';
|
||||
if (theme === 'dark') return 'system';
|
||||
return resolved === 'dark' ? 'light' : 'dark';
|
||||
};
|
||||
|
||||
const label =
|
||||
mode === 'light-dark-system'
|
||||
? `Switch theme (current: ${theme})`
|
||||
: `Switch to ${resolved === 'dark' ? 'light' : 'dark'} mode`;
|
||||
|
||||
return (
|
||||
<div className={cn('inline-flex', className)} {...props}>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={label}
|
||||
title={label}
|
||||
onClick={() => setTheme(nextTheme())}
|
||||
className="inline-flex size-8 items-center justify-center rounded-lg text-fd-muted-foreground transition-colors hover:bg-fd-accent hover:text-fd-accent-foreground"
|
||||
>
|
||||
{resolved === 'dark' ? <Moon className="size-4" /> : <Sun className="size-4" />}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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,66 +1,43 @@
|
||||
---
|
||||
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:
|
||||
- ./public/openapi.json
|
||||
toc:
|
||||
- depth: 2
|
||||
title: >-
|
||||
List every API token, enabled or not. The token value is never returned
|
||||
— only metadata.
|
||||
url: >-
|
||||
#list-every-api-token-enabled-or-not-the-token-value-is-never-returned--only-metadata
|
||||
title: List every API token, enabled or not. The token value is never returned —
|
||||
only metadata.
|
||||
url: '#list-every-api-token-enabled-or-not-the-token-value-is-never-returned--only-metadata'
|
||||
- depth: 2
|
||||
title: >-
|
||||
Mint a new API token. Name must be unique and 1-64 characters; the token
|
||||
string is server-generated and returned only in this response — it is
|
||||
stored hashed and cannot be retrieved later.
|
||||
url: >-
|
||||
#mint-a-new-api-token-name-must-be-unique-and-1-64-characters-the-token-string-is-server-generated-and-returned-only-in-this-response--it-is-stored-hashed-and-cannot-be-retrieved-later
|
||||
title: Mint a scoped API token. The server-generated plaintext is returned only
|
||||
once and stored as a hash.
|
||||
url: '#mint-a-scoped-api-token-the-server-generated-plaintext-is-returned-only-once-and-stored-as-a-hash'
|
||||
- depth: 2
|
||||
title: >-
|
||||
Permanently delete a token. Any caller using it stops authenticating
|
||||
title: Permanently delete a token. Any caller using it stops authenticating
|
||||
immediately.
|
||||
url: >-
|
||||
#permanently-delete-a-token-any-caller-using-it-stops-authenticating-immediately
|
||||
url: '#permanently-delete-a-token-any-caller-using-it-stops-authenticating-immediately'
|
||||
- depth: 2
|
||||
title: >-
|
||||
Toggle a token enabled/disabled without deleting it. Disabled tokens are
|
||||
title: Toggle a token enabled/disabled without deleting it. Disabled tokens are
|
||||
rejected by checkAPIAuth on the next request.
|
||||
url: >-
|
||||
#toggle-a-token-enableddisabled-without-deleting-it-disabled-tokens-are-rejected-by-checkapiauth-on-the-next-request
|
||||
url: '#toggle-a-token-enableddisabled-without-deleting-it-disabled-tokens-are-rejected-by-checkapiauth-on-the-next-request'
|
||||
structuredData:
|
||||
headings:
|
||||
- content: >-
|
||||
List every API token, enabled or not. The token value is never
|
||||
returned — only metadata.
|
||||
id: >-
|
||||
list-every-api-token-enabled-or-not-the-token-value-is-never-returned--only-metadata
|
||||
- content: >-
|
||||
Mint a new API token. Name must be unique and 1-64 characters; the
|
||||
token string is server-generated and returned only in this response —
|
||||
it is stored hashed and cannot be retrieved later.
|
||||
id: >-
|
||||
mint-a-new-api-token-name-must-be-unique-and-1-64-characters-the-token-string-is-server-generated-and-returned-only-in-this-response--it-is-stored-hashed-and-cannot-be-retrieved-later
|
||||
- content: >-
|
||||
Permanently delete a token. Any caller using it stops authenticating
|
||||
- content: List every API token, enabled or not. The token value is never returned
|
||||
— only metadata.
|
||||
id: list-every-api-token-enabled-or-not-the-token-value-is-never-returned--only-metadata
|
||||
- content: Mint a scoped API token. The server-generated plaintext is returned
|
||||
only once and stored as a hash.
|
||||
id: mint-a-scoped-api-token-the-server-generated-plaintext-is-returned-only-once-and-stored-as-a-hash
|
||||
- content: Permanently delete a token. Any caller using it stops authenticating
|
||||
immediately.
|
||||
id: >-
|
||||
permanently-delete-a-token-any-caller-using-it-stops-authenticating-immediately
|
||||
- content: >-
|
||||
Toggle a token enabled/disabled without deleting it. Disabled tokens
|
||||
id: permanently-delete-a-token-any-caller-using-it-stops-authenticating-immediately
|
||||
- content: Toggle a token enabled/disabled without deleting it. Disabled tokens
|
||||
are rejected by checkAPIAuth on the next request.
|
||||
id: >-
|
||||
toggle-a-token-enableddisabled-without-deleting-it-disabled-tokens-are-rejected-by-checkapiauth-on-the-next-request
|
||||
id: toggle-a-token-enableddisabled-without-deleting-it-disabled-tokens-are-rejected-by-checkapiauth-on-the-next-request
|
||||
contents: []
|
||||
---
|
||||
|
||||
|
||||
@@ -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 />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
---
|
||||
title: Inbounds
|
||||
description: >-
|
||||
Manage inbound configurations and their clients. All endpoints live under
|
||||
/panel/api/inbounds and require a logged-in session or Bearer token.
|
||||
description: Manage inbound configurations and their clients. All endpoints live
|
||||
under /panel/api/inbounds and require a logged-in session or Bearer token.
|
||||
Link-generating endpoints honour forwarded headers only when the request comes
|
||||
from a configured trusted proxy.
|
||||
full: true
|
||||
@@ -11,25 +10,20 @@ _openapi:
|
||||
- ./public/openapi.json
|
||||
toc:
|
||||
- depth: 2
|
||||
title: >-
|
||||
List every inbound owned by the authenticated user, including each
|
||||
title: List every inbound owned by the authenticated user, including each
|
||||
inbound’s clientStats traffic counters. settings, streamSettings, and
|
||||
sniffing are returned as nested JSON objects (no escaped strings);
|
||||
legacy callers that send them back as JSON-encoded strings are still
|
||||
accepted on write.
|
||||
url: >-
|
||||
#list-every-inbound-owned-by-the-authenticated-user-including-each-inbounds-clientstats-traffic-counters-settings-streamsettings-and-sniffing-are-returned-as-nested-json-objects-no-escaped-strings-legacy-callers-that-send-them-back-as-json-encoded-strings-are-still-accepted-on-write
|
||||
url: '#list-every-inbound-owned-by-the-authenticated-user-including-each-inbounds-clientstats-traffic-counters-settings-streamsettings-and-sniffing-are-returned-as-nested-json-objects-no-escaped-strings-legacy-callers-that-send-them-back-as-json-encoded-strings-are-still-accepted-on-write'
|
||||
- depth: 2
|
||||
title: >-
|
||||
Same shape as /list but with settings.clients[] stripped down to {email,
|
||||
title: Same shape as /list but with settings.clients[] stripped down to {email,
|
||||
enable, comment} and ClientStats not enriched with UUID/SubId. Use this
|
||||
for list pages; fetch /get/:id when you need the full per-client payload
|
||||
(uuid, password, flow, ...).
|
||||
url: >-
|
||||
#same-shape-as-list-but-with-settingsclients-stripped-down-to-email-enable-comment-and-clientstats-not-enriched-with-uuidsubid-use-this-for-list-pages-fetch-getid-when-you-need-the-full-per-client-payload-uuid-password-flow-
|
||||
url: '#same-shape-as-list-but-with-settingsclients-stripped-down-to-email-enable-comment-and-clientstats-not-enriched-with-uuidsubid-use-this-for-list-pages-fetch-getid-when-you-need-the-full-per-client-payload-uuid-password-flow-'
|
||||
- depth: 2
|
||||
title: >-
|
||||
Lightweight picker projection of the authenticated user’s inbounds.
|
||||
title: Lightweight picker projection of the authenticated user’s 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
|
||||
@@ -38,110 +32,95 @@ _openapi:
|
||||
dropdowns and attach pickers — it skips settings, streamSettings, and
|
||||
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
|
||||
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'
|
||||
- depth: 2
|
||||
title: >-
|
||||
Create a new inbound. Send the full inbound payload (protocol, port,
|
||||
title: Create a new inbound. Send the full inbound payload (protocol, port,
|
||||
settings, streamSettings, sniffing, remark, expiryTime, total, enable).
|
||||
settings, streamSettings, and sniffing may be sent as nested JSON
|
||||
objects (preferred) or as JSON-encoded strings (legacy).
|
||||
url: >-
|
||||
#create-a-new-inbound-send-the-full-inbound-payload-protocol-port-settings-streamsettings-sniffing-remark-expirytime-total-enable-settings-streamsettings-and-sniffing-may-be-sent-as-nested-json-objects-preferred-or-as-json-encoded-strings-legacy
|
||||
url: '#create-a-new-inbound-send-the-full-inbound-payload-protocol-port-settings-streamsettings-sniffing-remark-expirytime-total-enable-settings-streamsettings-and-sniffing-may-be-sent-as-nested-json-objects-preferred-or-as-json-encoded-strings-legacy'
|
||||
- depth: 2
|
||||
title: Delete an inbound by ID. Also removes its associated client stats rows.
|
||||
url: '#delete-an-inbound-by-id-also-removes-its-associated-client-stats-rows'
|
||||
- depth: 2
|
||||
title: >-
|
||||
Delete many inbounds in one call. Processes the list sequentially;
|
||||
title: Delete many inbounds in one call. Processes the list sequentially;
|
||||
failures are reported per id and the rest still proceed. Restarts xray
|
||||
at most once.
|
||||
url: >-
|
||||
#delete-many-inbounds-in-one-call-processes-the-list-sequentially-failures-are-reported-per-id-and-the-rest-still-proceed-restarts-xray-at-most-once
|
||||
url: '#delete-many-inbounds-in-one-call-processes-the-list-sequentially-failures-are-reported-per-id-and-the-rest-still-proceed-restarts-xray-at-most-once'
|
||||
- depth: 2
|
||||
title: >-
|
||||
Replace an inbound’s configuration. Body shape mirrors /add. Heavy on
|
||||
title: Replace an inbound’s configuration. Body shape mirrors /add. Heavy on
|
||||
inbounds with thousands of clients — prefer /setEnable for enable-only
|
||||
flips.
|
||||
url: >-
|
||||
#replace-an-inbounds-configuration-body-shape-mirrors-add-heavy-on-inbounds-with-thousands-of-clients--prefer-setenable-for-enable-only-flips
|
||||
url: '#replace-an-inbounds-configuration-body-shape-mirrors-add-heavy-on-inbounds-with-thousands-of-clients--prefer-setenable-for-enable-only-flips'
|
||||
- depth: 2
|
||||
title: >-
|
||||
Toggle only the enable flag without serialising the whole settings JSON.
|
||||
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
|
||||
url: '#toggle-only-the-enable-flag-without-serialising-the-whole-settings-json-recommended-for-ui-switches-on-large-inbounds'
|
||||
- depth: 2
|
||||
title: >-
|
||||
Zero out upload + download counters for a single inbound. Does not touch
|
||||
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.
|
||||
url: >-
|
||||
#zero-out-upload--download-counters-for-a-single-inbound-does-not-touch-per-client-counters
|
||||
url: '#zero-out-upload--download-counters-for-a-single-inbound-does-not-touch-per-client-counters'
|
||||
- depth: 2
|
||||
title: >-
|
||||
Remove every client attached to a single inbound while keeping the
|
||||
title: Remove every client attached to a single inbound while keeping the
|
||||
inbound itself. Collects emails from settings.clients[] and feeds them
|
||||
into the optimized bulk-delete path (runtime user removal + traffic-row
|
||||
cleanup + SyncInbound). Destructive and cannot be undone.
|
||||
url: >-
|
||||
#remove-every-client-attached-to-a-single-inbound-while-keeping-the-inbound-itself-collects-emails-from-settingsclients-and-feeds-them-into-the-optimized-bulk-delete-path-runtime-user-removal--traffic-row-cleanup--syncinbound-destructive-and-cannot-be-undone
|
||||
url: '#remove-every-client-attached-to-a-single-inbound-while-keeping-the-inbound-itself-collects-emails-from-settingsclients-and-feeds-them-into-the-optimized-bulk-delete-path-runtime-user-removal--traffic-row-cleanup--syncinbound-destructive-and-cannot-be-undone'
|
||||
- depth: 2
|
||||
title: >-
|
||||
Reset upload + download counters on every inbound. Destructive —
|
||||
title: Reset upload + download counters on every inbound. Destructive —
|
||||
accounting history is lost.
|
||||
url: >-
|
||||
#reset-upload--download-counters-on-every-inbound-destructive--accounting-history-is-lost
|
||||
url: '#reset-upload--download-counters-on-every-inbound-destructive--accounting-history-is-lost'
|
||||
- depth: 2
|
||||
title: >-
|
||||
Bulk-import an inbound from a JSON blob (e.g. one exported via the UI).
|
||||
title: Bulk-import an inbound from a JSON blob (e.g. one exported via the UI).
|
||||
The body uses form encoding with a single "data" field.
|
||||
url: >-
|
||||
#bulk-import-an-inbound-from-a-json-blob-eg-one-exported-via-the-ui-the-body-uses-form-encoding-with-a-single-data-field
|
||||
url: '#bulk-import-an-inbound-from-a-json-blob-eg-one-exported-via-the-ui-the-body-uses-form-encoding-with-a-single-data-field'
|
||||
- depth: 2
|
||||
title: >-
|
||||
Receive a master panel's aggregated per-client usage, keyed by the
|
||||
title: Receive a master panel's aggregated per-client usage, keyed by the
|
||||
master's GUID. Stored in a side table used only for the UI display
|
||||
overlay and local quota enforcement — never folded into the local
|
||||
counters that masters poll, so delta accounting stays intact. Called
|
||||
panel-to-panel by the node traffic sync job.
|
||||
url: >-
|
||||
#receive-a-master-panels-aggregated-per-client-usage-keyed-by-the-masters-guid-stored-in-a-side-table-used-only-for-the-ui-display-overlay-and-local-quota-enforcement--never-folded-into-the-local-counters-that-masters-poll-so-delta-accounting-stays-intact-called-panel-to-panel-by-the-node-traffic-sync-job
|
||||
url: '#receive-a-master-panels-aggregated-per-client-usage-keyed-by-the-masters-guid-stored-in-a-side-table-used-only-for-the-ui-display-overlay-and-local-quota-enforcement--never-folded-into-the-local-counters-that-masters-poll-so-delta-accounting-stays-intact-called-panel-to-panel-by-the-node-traffic-sync-job'
|
||||
- depth: 2
|
||||
title: >-
|
||||
List the fallback rules attached to a master VLESS/Trojan TCP-TLS
|
||||
title: List the fallback rules attached to a master VLESS/Trojan TCP-TLS
|
||||
inbound. Each rule links one child inbound (the dest) to optional
|
||||
SNI/ALPN/path/dest/xver match criteria. When dest is empty the child
|
||||
inbound's listen+port is used.
|
||||
url: >-
|
||||
#list-the-fallback-rules-attached-to-a-master-vlesstrojan-tcp-tls-inbound-each-rule-links-one-child-inbound-the-dest-to-optional-snialpnpathdestxver-match-criteria-when-dest-is-empty-the-child-inbounds-listenport-is-used
|
||||
url: '#list-the-fallback-rules-attached-to-a-master-vlesstrojan-tcp-tls-inbound-each-rule-links-one-child-inbound-the-dest-to-optional-snialpnpathdestxver-match-criteria-when-dest-is-empty-the-child-inbounds-listenport-is-used'
|
||||
- depth: 2
|
||||
title: >-
|
||||
Replace the entire fallback list for a master inbound. Body is JSON.
|
||||
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
|
||||
url: '#replace-the-entire-fallback-list-for-a-master-inbound-body-is-json-triggers-an-xray-restart'
|
||||
structuredData:
|
||||
headings:
|
||||
- content: >-
|
||||
List every inbound owned by the authenticated user, including each
|
||||
- content: List every inbound owned by the authenticated user, including each
|
||||
inbound’s clientStats traffic counters. settings, streamSettings, and
|
||||
sniffing are returned as nested JSON objects (no escaped strings);
|
||||
legacy callers that send them back as JSON-encoded strings are still
|
||||
accepted on write.
|
||||
id: >-
|
||||
list-every-inbound-owned-by-the-authenticated-user-including-each-inbounds-clientstats-traffic-counters-settings-streamsettings-and-sniffing-are-returned-as-nested-json-objects-no-escaped-strings-legacy-callers-that-send-them-back-as-json-encoded-strings-are-still-accepted-on-write
|
||||
- content: >-
|
||||
Same shape as /list but with settings.clients[] stripped down to
|
||||
id: list-every-inbound-owned-by-the-authenticated-user-including-each-inbounds-clientstats-traffic-counters-settings-streamsettings-and-sniffing-are-returned-as-nested-json-objects-no-escaped-strings-legacy-callers-that-send-them-back-as-json-encoded-strings-are-still-accepted-on-write
|
||||
- content: Same shape as /list but with settings.clients[] stripped down to
|
||||
{email, enable, comment} and ClientStats not enriched with UUID/SubId.
|
||||
Use this for list pages; fetch /get/:id when you need the full
|
||||
per-client payload (uuid, password, flow, ...).
|
||||
id: >-
|
||||
same-shape-as-list-but-with-settingsclients-stripped-down-to-email-enable-comment-and-clientstats-not-enriched-with-uuidsubid-use-this-for-list-pages-fetch-getid-when-you-need-the-full-per-client-payload-uuid-password-flow-
|
||||
- content: >-
|
||||
Lightweight picker projection of the authenticated user’s inbounds.
|
||||
id: same-shape-as-list-but-with-settingsclients-stripped-down-to-email-enable-comment-and-clientstats-not-enriched-with-uuidsubid-use-this-for-list-pages-fetch-getid-when-you-need-the-full-per-client-payload-uuid-password-flow-
|
||||
- content: Lightweight picker projection of the authenticated user’s 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
|
||||
@@ -150,80 +129,66 @@ _openapi:
|
||||
dropdowns and attach pickers — it skips settings, streamSettings, and
|
||||
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
|
||||
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,
|
||||
- content: Create a new inbound. Send the full inbound payload (protocol, port,
|
||||
settings, streamSettings, sniffing, remark, expiryTime, total,
|
||||
enable). settings, streamSettings, and sniffing may be sent as nested
|
||||
JSON objects (preferred) or as JSON-encoded strings (legacy).
|
||||
id: >-
|
||||
create-a-new-inbound-send-the-full-inbound-payload-protocol-port-settings-streamsettings-sniffing-remark-expirytime-total-enable-settings-streamsettings-and-sniffing-may-be-sent-as-nested-json-objects-preferred-or-as-json-encoded-strings-legacy
|
||||
- content: >-
|
||||
Delete an inbound by ID. Also removes its associated client stats
|
||||
rows.
|
||||
id: create-a-new-inbound-send-the-full-inbound-payload-protocol-port-settings-streamsettings-sniffing-remark-expirytime-total-enable-settings-streamsettings-and-sniffing-may-be-sent-as-nested-json-objects-preferred-or-as-json-encoded-strings-legacy
|
||||
- content: Delete an inbound by ID. Also removes its associated client stats rows.
|
||||
id: delete-an-inbound-by-id-also-removes-its-associated-client-stats-rows
|
||||
- content: >-
|
||||
Delete many inbounds in one call. Processes the list sequentially;
|
||||
- content: Delete many inbounds in one call. Processes the list sequentially;
|
||||
failures are reported per id and the rest still proceed. Restarts xray
|
||||
at most once.
|
||||
id: >-
|
||||
delete-many-inbounds-in-one-call-processes-the-list-sequentially-failures-are-reported-per-id-and-the-rest-still-proceed-restarts-xray-at-most-once
|
||||
- content: >-
|
||||
Replace an inbound’s configuration. Body shape mirrors /add. Heavy on
|
||||
id: delete-many-inbounds-in-one-call-processes-the-list-sequentially-failures-are-reported-per-id-and-the-rest-still-proceed-restarts-xray-at-most-once
|
||||
- content: Replace an inbound’s configuration. Body shape mirrors /add. Heavy on
|
||||
inbounds with thousands of clients — prefer /setEnable for enable-only
|
||||
flips.
|
||||
id: >-
|
||||
replace-an-inbounds-configuration-body-shape-mirrors-add-heavy-on-inbounds-with-thousands-of-clients--prefer-setenable-for-enable-only-flips
|
||||
- content: >-
|
||||
Toggle only the enable flag without serialising the whole settings
|
||||
id: replace-an-inbounds-configuration-body-shape-mirrors-add-heavy-on-inbounds-with-thousands-of-clients--prefer-setenable-for-enable-only-flips
|
||||
- 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: >-
|
||||
Zero out upload + download counters for a single inbound. Does not
|
||||
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
|
||||
- content: >-
|
||||
Remove every client attached to a single inbound while keeping the
|
||||
id: zero-out-upload--download-counters-for-a-single-inbound-does-not-touch-per-client-counters
|
||||
- content: Remove every client attached to a single inbound while keeping the
|
||||
inbound itself. Collects emails from settings.clients[] and feeds them
|
||||
into the optimized bulk-delete path (runtime user removal +
|
||||
traffic-row cleanup + SyncInbound). Destructive and cannot be undone.
|
||||
id: >-
|
||||
remove-every-client-attached-to-a-single-inbound-while-keeping-the-inbound-itself-collects-emails-from-settingsclients-and-feeds-them-into-the-optimized-bulk-delete-path-runtime-user-removal--traffic-row-cleanup--syncinbound-destructive-and-cannot-be-undone
|
||||
- content: >-
|
||||
Reset upload + download counters on every inbound. Destructive —
|
||||
id: remove-every-client-attached-to-a-single-inbound-while-keeping-the-inbound-itself-collects-emails-from-settingsclients-and-feeds-them-into-the-optimized-bulk-delete-path-runtime-user-removal--traffic-row-cleanup--syncinbound-destructive-and-cannot-be-undone
|
||||
- content: Reset upload + download counters on every inbound. Destructive —
|
||||
accounting history is lost.
|
||||
id: >-
|
||||
reset-upload--download-counters-on-every-inbound-destructive--accounting-history-is-lost
|
||||
- content: >-
|
||||
Bulk-import an inbound from a JSON blob (e.g. one exported via the
|
||||
UI). The body uses form encoding with a single "data" field.
|
||||
id: >-
|
||||
bulk-import-an-inbound-from-a-json-blob-eg-one-exported-via-the-ui-the-body-uses-form-encoding-with-a-single-data-field
|
||||
- content: >-
|
||||
Receive a master panel's aggregated per-client usage, keyed by the
|
||||
id: reset-upload--download-counters-on-every-inbound-destructive--accounting-history-is-lost
|
||||
- content: Bulk-import an inbound from a JSON blob (e.g. one exported via the UI).
|
||||
The body uses form encoding with a single "data" field.
|
||||
id: bulk-import-an-inbound-from-a-json-blob-eg-one-exported-via-the-ui-the-body-uses-form-encoding-with-a-single-data-field
|
||||
- content: Receive a master panel's aggregated per-client usage, keyed by the
|
||||
master's GUID. Stored in a side table used only for the UI display
|
||||
overlay and local quota enforcement — never folded into the local
|
||||
counters that masters poll, so delta accounting stays intact. Called
|
||||
panel-to-panel by the node traffic sync job.
|
||||
id: >-
|
||||
receive-a-master-panels-aggregated-per-client-usage-keyed-by-the-masters-guid-stored-in-a-side-table-used-only-for-the-ui-display-overlay-and-local-quota-enforcement--never-folded-into-the-local-counters-that-masters-poll-so-delta-accounting-stays-intact-called-panel-to-panel-by-the-node-traffic-sync-job
|
||||
- content: >-
|
||||
List the fallback rules attached to a master VLESS/Trojan TCP-TLS
|
||||
id: receive-a-master-panels-aggregated-per-client-usage-keyed-by-the-masters-guid-stored-in-a-side-table-used-only-for-the-ui-display-overlay-and-local-quota-enforcement--never-folded-into-the-local-counters-that-masters-poll-so-delta-accounting-stays-intact-called-panel-to-panel-by-the-node-traffic-sync-job
|
||||
- content: List the fallback rules attached to a master VLESS/Trojan TCP-TLS
|
||||
inbound. Each rule links one child inbound (the dest) to optional
|
||||
SNI/ALPN/path/dest/xver match criteria. When dest is empty the child
|
||||
inbound's listen+port is used.
|
||||
id: >-
|
||||
list-the-fallback-rules-attached-to-a-master-vlesstrojan-tcp-tls-inbound-each-rule-links-one-child-inbound-the-dest-to-optional-snialpnpathdestxver-match-criteria-when-dest-is-empty-the-child-inbounds-listenport-is-used
|
||||
- content: >-
|
||||
Replace the entire fallback list for a master inbound. Body is JSON.
|
||||
id: list-the-fallback-rules-attached-to-a-master-vlesstrojan-tcp-tls-inbound-each-rule-links-one-child-inbound-the-dest-to-optional-snialpnpathdestxver-match-criteria-when-dest-is-empty-the-child-inbounds-listenport-is-used
|
||||
- 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
|
||||
id: replace-the-entire-fallback-list-for-a-master-inbound-body-is-json-triggers-an-xray-restart
|
||||
contents: []
|
||||
---
|
||||
|
||||
@@ -236,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"}]} 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",
|
||||
|
||||
@@ -1,54 +1,51 @@
|
||||
---
|
||||
title: Nodes
|
||||
description: >-
|
||||
Manage remote 3x-ui panels acting as nodes for a central panel. All endpoints
|
||||
under /panel/api/nodes.
|
||||
description: Manage remote 3x-ui panels acting as nodes for a central panel. All
|
||||
endpoints under /panel/api/nodes.
|
||||
full: true
|
||||
_openapi:
|
||||
preload:
|
||||
- ./public/openapi.json
|
||||
toc:
|
||||
- depth: 2
|
||||
title: >-
|
||||
List every configured node with its connection details, health, and last
|
||||
title: List every configured node with its connection details, health, and last
|
||||
heartbeat patch.
|
||||
url: >-
|
||||
#list-every-configured-node-with-its-connection-details-health-and-last-heartbeat-patch
|
||||
url: '#list-every-configured-node-with-its-connection-details-health-and-last-heartbeat-patch'
|
||||
- depth: 2
|
||||
title: >-
|
||||
This panel's node-auth CA certificate (public, PEM) to paste into a
|
||||
title: This panel's node-auth CA certificate (public, PEM) to paste into a
|
||||
node's mTLS trust setting. Lazily mints the CA and the master client
|
||||
cert on first call. Pair with setting tlsVerifyMode=mtls on the node.
|
||||
url: >-
|
||||
#this-panels-node-auth-ca-certificate-public-pem-to-paste-into-a-nodes-mtls-trust-setting-lazily-mints-the-ca-and-the-master-client-cert-on-first-call-pair-with-setting-tlsverifymodemtls-on-the-node
|
||||
url: '#this-panels-node-auth-ca-certificate-public-pem-to-paste-into-a-nodes-mtls-trust-setting-lazily-mints-the-ca-and-the-master-client-cert-on-first-call-pair-with-setting-tlsverifymodemtls-on-the-node'
|
||||
- depth: 2
|
||||
title: >-
|
||||
Set the CA certificate this panel trusts for incoming node-API client
|
||||
title: Set the CA certificate this panel trusts for incoming node-API client
|
||||
certificates (this panel acting as a node). Paste the managing panel's
|
||||
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
|
||||
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'
|
||||
- depth: 2
|
||||
title: >-
|
||||
Fetch a node's own web TLS certificate/key file paths (proxied to the
|
||||
title: Fetch a node's own web TLS certificate/key file paths (proxied to the
|
||||
node). Used by the inbound form's "Set Cert from Panel" so a
|
||||
node-assigned inbound gets paths that exist on the node, not the central
|
||||
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
|
||||
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'
|
||||
@@ -56,115 +53,92 @@ _openapi:
|
||||
title: Pause or resume traffic sync with this node.
|
||||
url: '#pause-or-resume-traffic-sync-with-this-node'
|
||||
- depth: 2
|
||||
title: >-
|
||||
Probe a node without saving it. Uses the body as connection details and
|
||||
title: Probe a node without saving it. Uses the body as connection details and
|
||||
returns the same heartbeat snapshot a registered node would have.
|
||||
url: >-
|
||||
#probe-a-node-without-saving-it-uses-the-body-as-connection-details-and-returns-the-same-heartbeat-snapshot-a-registered-node-would-have
|
||||
url: '#probe-a-node-without-saving-it-uses-the-body-as-connection-details-and-returns-the-same-heartbeat-snapshot-a-registered-node-would-have'
|
||||
- depth: 2
|
||||
title: >-
|
||||
Connect to the node over HTTPS without verifying its certificate and
|
||||
title: Connect to the node over HTTPS without verifying its certificate and
|
||||
return the leaf certificate's SHA-256 (base64). Used by the Add/Edit
|
||||
Node dialog to fetch and pin a self-signed certificate. Uses the same
|
||||
body as /test.
|
||||
url: >-
|
||||
#connect-to-the-node-over-https-without-verifying-its-certificate-and-return-the-leaf-certificates-sha-256-base64-used-by-the-addedit-node-dialog-to-fetch-and-pin-a-self-signed-certificate-uses-the-same-body-as-test
|
||||
url: '#connect-to-the-node-over-https-without-verifying-its-certificate-and-return-the-leaf-certificates-sha-256-base64-used-by-the-addedit-node-dialog-to-fetch-and-pin-a-self-signed-certificate-uses-the-same-body-as-test'
|
||||
- depth: 2
|
||||
title: >-
|
||||
Use unsaved node connection details to list the remote inbounds
|
||||
available for selective import.
|
||||
url: >-
|
||||
#use-unsaved-node-connection-details-to-list-the-remote-inbounds-available-for-selective-import
|
||||
title: Use unsaved node connection details to list the remote inbounds available
|
||||
for selective import.
|
||||
url: '#use-unsaved-node-connection-details-to-list-the-remote-inbounds-available-for-selective-import'
|
||||
- depth: 2
|
||||
title: Probe an existing node, updating its cached health state.
|
||||
url: '#probe-an-existing-node-updating-its-cached-health-state'
|
||||
- depth: 2
|
||||
title: >-
|
||||
Trigger the official panel self-updater on each given node (downloads
|
||||
title: 'Trigger the official panel self-updater on each given node (downloads
|
||||
the latest release and restarts). Only enabled, online nodes are
|
||||
updated; offline/disabled ones are reported as skipped. Set "dev": true
|
||||
to move the nodes to the rolling per-commit dev channel instead of the
|
||||
latest stable release. Returns a per-node result list.
|
||||
url: >-
|
||||
#trigger-the-official-panel-self-updater-on-each-given-node-downloads-the-latest-release-and-restarts-only-enabled-online-nodes-are-updated-offlinedisabled-ones-are-reported-as-skipped-set-dev-true-to-move-the-nodes-to-the-rolling-per-commit-dev-channel-instead-of-the-latest-stable-release-returns-a-per-node-result-list
|
||||
latest stable release. Returns a per-node result list.'
|
||||
url: '#trigger-the-official-panel-self-updater-on-each-given-node-downloads-the-latest-release-and-restarts-only-enabled-online-nodes-are-updated-offlinedisabled-ones-are-reported-as-skipped-set-dev-true-to-move-the-nodes-to-the-rolling-per-commit-dev-channel-instead-of-the-latest-stable-release-returns-a-per-node-result-list'
|
||||
- depth: 2
|
||||
title: >-
|
||||
Aggregated metric history for a node — same shape as /server/history,
|
||||
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
|
||||
url: '#aggregated-metric-history-for-a-node--same-shape-as-serverhistory-scoped-to-one-node'
|
||||
structuredData:
|
||||
headings:
|
||||
- content: >-
|
||||
List every configured node with its connection details, health, and
|
||||
- content: List every configured node with its connection details, health, and
|
||||
last heartbeat patch.
|
||||
id: >-
|
||||
list-every-configured-node-with-its-connection-details-health-and-last-heartbeat-patch
|
||||
- content: >-
|
||||
This panel's node-auth CA certificate (public, PEM) to paste into a
|
||||
id: list-every-configured-node-with-its-connection-details-health-and-last-heartbeat-patch
|
||||
- content: This panel's node-auth CA certificate (public, PEM) to paste into a
|
||||
node's mTLS trust setting. Lazily mints the CA and the master client
|
||||
cert on first call. Pair with setting tlsVerifyMode=mtls on the node.
|
||||
id: >-
|
||||
this-panels-node-auth-ca-certificate-public-pem-to-paste-into-a-nodes-mtls-trust-setting-lazily-mints-the-ca-and-the-master-client-cert-on-first-call-pair-with-setting-tlsverifymodemtls-on-the-node
|
||||
- content: >-
|
||||
Set the CA certificate this panel trusts for incoming node-API client
|
||||
id: this-panels-node-auth-ca-certificate-public-pem-to-paste-into-a-nodes-mtls-trust-setting-lazily-mints-the-ca-and-the-master-client-cert-on-first-call-pair-with-setting-tlsverifymodemtls-on-the-node
|
||||
- content: Set the CA certificate this panel trusts for incoming node-API client
|
||||
certificates (this panel acting as a node). Paste the managing panel's
|
||||
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
|
||||
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
|
||||
- content: Fetch a node's own web TLS certificate/key file paths (proxied to the
|
||||
node). Used by the inbound form's "Set Cert from Panel" so a
|
||||
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
|
||||
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, 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.
|
||||
id: pause-or-resume-traffic-sync-with-this-node
|
||||
- content: >-
|
||||
Probe a node without saving it. Uses the body as connection details
|
||||
and returns the same heartbeat snapshot a registered node would have.
|
||||
id: >-
|
||||
probe-a-node-without-saving-it-uses-the-body-as-connection-details-and-returns-the-same-heartbeat-snapshot-a-registered-node-would-have
|
||||
- content: >-
|
||||
Connect to the node over HTTPS without verifying its certificate and
|
||||
- content: Probe a node without saving it. Uses the body as connection details and
|
||||
returns the same heartbeat snapshot a registered node would have.
|
||||
id: probe-a-node-without-saving-it-uses-the-body-as-connection-details-and-returns-the-same-heartbeat-snapshot-a-registered-node-would-have
|
||||
- content: Connect to the node over HTTPS without verifying its certificate and
|
||||
return the leaf certificate's SHA-256 (base64). Used by the Add/Edit
|
||||
Node dialog to fetch and pin a self-signed certificate. Uses the same
|
||||
body as /test.
|
||||
id: >-
|
||||
connect-to-the-node-over-https-without-verifying-its-certificate-and-return-the-leaf-certificates-sha-256-base64-used-by-the-addedit-node-dialog-to-fetch-and-pin-a-self-signed-certificate-uses-the-same-body-as-test
|
||||
- content: >-
|
||||
Use unsaved node connection details to list the remote inbounds
|
||||
id: connect-to-the-node-over-https-without-verifying-its-certificate-and-return-the-leaf-certificates-sha-256-base64-used-by-the-addedit-node-dialog-to-fetch-and-pin-a-self-signed-certificate-uses-the-same-body-as-test
|
||||
- content: Use unsaved node connection details to list the remote inbounds
|
||||
available for selective import.
|
||||
id: >-
|
||||
use-unsaved-node-connection-details-to-list-the-remote-inbounds-available-for-selective-import
|
||||
id: use-unsaved-node-connection-details-to-list-the-remote-inbounds-available-for-selective-import
|
||||
- content: Probe an existing node, updating its cached health state.
|
||||
id: probe-an-existing-node-updating-its-cached-health-state
|
||||
- content: >-
|
||||
Trigger the official panel self-updater on each given node (downloads
|
||||
- content: 'Trigger the official panel self-updater on each given node (downloads
|
||||
the latest release and restarts). Only enabled, online nodes are
|
||||
updated; offline/disabled ones are reported as skipped. Set "dev":
|
||||
true to move the nodes to the rolling per-commit dev channel instead
|
||||
of the latest stable release. Returns a per-node result list.
|
||||
id: >-
|
||||
trigger-the-official-panel-self-updater-on-each-given-node-downloads-the-latest-release-and-restarts-only-enabled-online-nodes-are-updated-offlinedisabled-ones-are-reported-as-skipped-set-dev-true-to-move-the-nodes-to-the-rolling-per-commit-dev-channel-instead-of-the-latest-stable-release-returns-a-per-node-result-list
|
||||
- content: >-
|
||||
Aggregated metric history for a node — same shape as /server/history,
|
||||
of the latest stable release. Returns a per-node result list.'
|
||||
id: trigger-the-official-panel-self-updater-on-each-given-node-downloads-the-latest-release-and-restarts-only-enabled-online-nodes-are-updated-offlinedisabled-ones-are-reported-as-skipped-set-dev-true-to-move-the-nodes-to-the-rolling-per-commit-dev-channel-instead-of-the-latest-stable-release-returns-a-per-node-result-list
|
||||
- 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
|
||||
id: aggregated-metric-history-for-a-node--same-shape-as-serverhistory-scoped-to-one-node
|
||||
contents: []
|
||||
---
|
||||
|
||||
@@ -177,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"}]} 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;
|
||||
@@ -2,7 +2,15 @@ import type { BaseLayoutProps } from 'fumadocs-ui/layouts/shared';
|
||||
import { Heart } from 'lucide-react';
|
||||
import { Logo } from '@/components/logo';
|
||||
import { TelegramIcon } from '@/components/icons';
|
||||
import { appName, productRepoUrl, telegramChannel, telegramChannelUrl, donateUrl, siteUrl } from './shared';
|
||||
import { DocsThemeSwitch } from '@/components/theme-switch';
|
||||
import {
|
||||
appName,
|
||||
productRepoUrl,
|
||||
telegramChannel,
|
||||
telegramChannelUrl,
|
||||
donateUrl,
|
||||
siteUrl,
|
||||
} from './shared';
|
||||
import { getSiteMessages } from './site-i18n';
|
||||
|
||||
// Build locale-aware shared layout options. With `hideLocale: 'default-locale'`,
|
||||
@@ -12,6 +20,9 @@ export function baseOptions(lang: string): BaseLayoutProps {
|
||||
const m = getSiteMessages(lang);
|
||||
|
||||
return {
|
||||
slots: {
|
||||
themeSwitch: DocsThemeSwitch,
|
||||
},
|
||||
nav: {
|
||||
title: (
|
||||
<span className="inline-flex items-center gap-2 font-semibold">
|
||||
|
||||
@@ -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);
|
||||
|
||||
+20
-21
@@ -11,42 +11,41 @@
|
||||
"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.11.5",
|
||||
"fumadocs-core": "^16.14.5",
|
||||
"fumadocs-docgen": "^3.1.0",
|
||||
"fumadocs-mdx": "^15.2.0",
|
||||
"fumadocs-openapi": "^11.2.2",
|
||||
"fumadocs-ui": "^16.11.5",
|
||||
"lucide-react": "^1.25.0",
|
||||
"mermaid": "^11.16.0",
|
||||
"next": "16.2.11",
|
||||
"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": "4.0.0",
|
||||
"zod": "^4.4.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4.3.3",
|
||||
"@types/mdx": "^2.0.14",
|
||||
"@types/node": "^26.1.1",
|
||||
"@types/react": "^19.2.17",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"eslint": "^9.39.5",
|
||||
"eslint-config-next": "16.2.11",
|
||||
"postcss": "^8.5.21",
|
||||
"prettier": "^3.9.6",
|
||||
"@types/node": "^26.2.0",
|
||||
"@types/react": "^19.2.18",
|
||||
"@types/react-dom": "^19.2.4",
|
||||
"oxfmt": "0.64.0",
|
||||
"oxlint": "1.79.0",
|
||||
"postcss": "^8.5.26",
|
||||
"tailwindcss": "^4.3.3",
|
||||
"typescript": "^6.0.3",
|
||||
"vitest": "^4.1.10"
|
||||
"typescript": "7.0.2",
|
||||
"vitest": "^4.1.11"
|
||||
},
|
||||
"packageManager": "pnpm@11.15.1+sha512.81350b07e53c9538a02f1f2303b4290fa2d7be04e56e2a970c4cc4b417dc761de196edabd49d55c7dc9580db81007c44143e4e3d7e462b3000d23c255122d065"
|
||||
"packageManager": "pnpm@11.22.0+sha512.1ff870c4c6133dfd88fb2afc46dd13d47f09c9794b438c6fdb47ca98caf3bc16381ee0be93a091b8e3824cf01f889f46d7d9e20910fb0be1ab0fb5baa80dd621"
|
||||
}
|
||||
|
||||
Generated
+1564
-3629
File diff suppressed because it is too large
Load Diff
@@ -8,5 +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
|
||||
- '@mermaid-js/parser@1.2.1'
|
||||
- mermaid@11.17.0
|
||||
- lucide-react@1.33.0
|
||||
- postcss@8.5.26
|
||||
- fumadocs-mdx@15.3.0
|
||||
|
||||
+3235
-384
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,57 @@
|
||||
# PWA installability verification
|
||||
|
||||
This change adds a network-only PWA surface to the login and panel pages. It
|
||||
does not cache panel data, API responses, credentials, or WebSocket traffic.
|
||||
|
||||
## Local checks
|
||||
|
||||
Run these commands from the repository root after installing the pinned Node
|
||||
and Go toolchains:
|
||||
|
||||
```text
|
||||
cd frontend
|
||||
npm run typecheck
|
||||
npm run lint
|
||||
npx vitest run --project unit
|
||||
npx vitest run --project components
|
||||
npm run build
|
||||
cd ..
|
||||
go test ./...
|
||||
go build ./...
|
||||
```
|
||||
|
||||
The built binary must serve these paths beneath the configured `webBasePath`:
|
||||
|
||||
- `manifest.webmanifest`
|
||||
- `pwa-register.js`
|
||||
- `service-worker.js`
|
||||
- `icons/3x-ui-16.png`
|
||||
- `icons/3x-ui-24.png`
|
||||
- `icons/3x-ui-32.png`
|
||||
- `icons/3x-ui-64.png`
|
||||
- `icons/3x-ui-192.png`
|
||||
- `icons/3x-ui-512.png`
|
||||
|
||||
The login and panel HTML must contain a manifest link and registration script
|
||||
whose URLs begin with the same runtime base path. The manifest must contain
|
||||
`display: "standalone"`, relative `start_url` and `scope`, and all six icon
|
||||
entries.
|
||||
|
||||
## Live rollout checks
|
||||
|
||||
Before replacing a server binary, record the current x-ui binary checksum and
|
||||
create a timestamped copy of the binary and `/etc/x-ui/x-ui.db`. Restart only
|
||||
the `x-ui` service after the candidate is staged. Because x-ui manages Xray as
|
||||
a child process, the restart can briefly interrupt VPN connections.
|
||||
|
||||
After the restart, verify:
|
||||
|
||||
1. `x-ui` is active and its child Xray process is running.
|
||||
2. The existing panel URL serves HTML with the PWA manifest link.
|
||||
3. The manifest, registration script, worker, and all six icons return `200`.
|
||||
4. Login, authenticated API requests, panel navigation, logout, and the panel
|
||||
WebSocket all work.
|
||||
5. At least one VPN client can complete a fresh connection cycle.
|
||||
|
||||
If any check fails, restore the exact binary backup, restart x-ui once, and
|
||||
repeat the checks against the original build.
|
||||
+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,4 +1,4 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useLayoutEffect } from 'react';
|
||||
import type { Decorator, Preview } from '@storybook/react-vite';
|
||||
import { ConfigProvider } from 'antd';
|
||||
import i18next from 'i18next';
|
||||
@@ -17,11 +17,12 @@ if (!i18next.isInitialized) {
|
||||
});
|
||||
}
|
||||
|
||||
const withTheme: Decorator = (Story, context) => {
|
||||
export const withTheme: Decorator = (Story, context) => {
|
||||
const dark = context.globals.theme === 'dark';
|
||||
useEffect(() => {
|
||||
document.body.setAttribute('class', dark ? 'dark' : 'light');
|
||||
document.documentElement.setAttribute('data-theme', dark ? 'dark' : 'light');
|
||||
useLayoutEffect(() => {
|
||||
document.body.classList.remove('dark', 'light');
|
||||
document.body.classList.add(dark ? 'dark' : 'light');
|
||||
document.documentElement.removeAttribute('data-theme');
|
||||
}, [dark]);
|
||||
return (
|
||||
<ConfigProvider theme={buildAntdThemeConfig(dark, false)}>
|
||||
|
||||
+3
-2
@@ -31,8 +31,9 @@ The `@` import alias maps to `src/`.
|
||||
Form *state* runs on React Hook Form (`src/components/form/rhf/`), not Ant
|
||||
Design's `Form` store.
|
||||
- Function components + hooks only; no class components.
|
||||
- No `//` line comments in committed TS/TSX. HTML comments are fine.
|
||||
- TS strict; `no-explicit-any` is an error. Build forms with `useZodForm` +
|
||||
- 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; 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
|
||||
|
||||
+27
-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
|
||||
|
||||
@@ -70,15 +74,27 @@ react-query into separate vendor bundles to keep the per-page
|
||||
initial JS small. The Go binary embeds this directory at compile
|
||||
time and `internal/web/controller/dist.go` serves the per-page HTML.
|
||||
|
||||
### PWA mode
|
||||
|
||||
The login and panel pages expose a minimal network-only Progressive Web App.
|
||||
The manifest, service worker, registration script, and icons are embedded with
|
||||
the frontend and served under the runtime `webBasePath`. The service worker
|
||||
does not use Cache Storage, does not intercept requests, and does not provide
|
||||
offline access; panel authentication, API calls, and WebSocket traffic remain
|
||||
normal network requests.
|
||||
|
||||
## Layout
|
||||
|
||||
```
|
||||
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)
|
||||
@@ -146,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
+2336
-3625
File diff suppressed because it is too large
Load Diff
+42
-40
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "3x-ui-frontend",
|
||||
"private": true,
|
||||
"version": "0.4.3",
|
||||
"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.4.3",
|
||||
"@noble/hashes": "^2.2.0",
|
||||
"@tanstack/react-query": "^5.101.4",
|
||||
"@tanstack/react-query-devtools": "^5.101.4",
|
||||
"antd": "^6.5.2",
|
||||
"@hookform/resolvers": "^5.9.1",
|
||||
"@noble/hashes": "^2.3.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.5",
|
||||
"persian-calendar-suite": "^1.5.6",
|
||||
"react": "^19.2.8",
|
||||
"react-dom": "^19.2.8",
|
||||
"react-hook-form": "^7.83.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.11",
|
||||
"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.4",
|
||||
"@storybook/addon-docs": "^10.5.4",
|
||||
"@storybook/addon-vitest": "^10.5.4",
|
||||
"@storybook/react-vite": "^10.5.4",
|
||||
"@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.17",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@types/react": "^19.2.18",
|
||||
"@types/react-dom": "^19.2.5",
|
||||
"@types/swagger-ui-react": "^5.18.0",
|
||||
"@vitejs/plugin-react": "^6.0.4",
|
||||
"@vitest/browser-playwright": "4.1.10",
|
||||
"@vitest/coverage-v8": "^4.1.10",
|
||||
"eslint": "^10.8.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.10.2",
|
||||
"eslint-plugin-react-hooks": "^7.1.1",
|
||||
"globals": "^17.7.0",
|
||||
"@vitejs/plugin-react": "^6.1.0",
|
||||
"@vitest/browser-playwright": "4.1.11",
|
||||
"@vitest/coverage-v8": "^4.1.11",
|
||||
"husky": "^9.1.7",
|
||||
"jsdom": "^29.1.1",
|
||||
"lint-staged": "^17.2.0",
|
||||
"jsdom": "^30.0.1",
|
||||
"lint-staged": "^17.3.0",
|
||||
"msw": "^2.15.0",
|
||||
"playwright": "^1.62.0",
|
||||
"storybook": "^10.5.4",
|
||||
"typescript": "6.0.3",
|
||||
"typescript-eslint": "^8.65.0",
|
||||
"vite": "8.1.5",
|
||||
"vitest": "^4.1.10"
|
||||
"oxfmt": "0.64.0",
|
||||
"oxlint": "1.79.0",
|
||||
"oxlint-tsgolint": "^7.0.2001",
|
||||
"playwright": "^1.62.1",
|
||||
"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",
|
||||
@@ -89,7 +90,8 @@
|
||||
"react": "^19.0.0"
|
||||
},
|
||||
"swagger-ui-react": {
|
||||
"js-yaml": "^4.2.0"
|
||||
"js-yaml": "^4.2.0",
|
||||
"brace-expansion": "^5.0.9"
|
||||
},
|
||||
"@typeschema/valibot": {
|
||||
"valibot": "^1.1.0"
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 3.1 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 37 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 3.4 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 3.8 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 270 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 6.7 KiB |
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"name": "3x-ui",
|
||||
"short_name": "3x-ui",
|
||||
"start_url": "./",
|
||||
"scope": "./",
|
||||
"display": "standalone",
|
||||
"background_color": "#0f172a",
|
||||
"theme_color": "#1677ff",
|
||||
"icons": [
|
||||
{
|
||||
"src": "icons/3x-ui-16.png",
|
||||
"sizes": "16x16",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "icons/3x-ui-24.png",
|
||||
"sizes": "24x24",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "icons/3x-ui-32.png",
|
||||
"sizes": "32x32",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "icons/3x-ui-64.png",
|
||||
"sizes": "64x64",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "icons/3x-ui-192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "icons/3x-ui-512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -7,8 +7,8 @@
|
||||
* - Please do NOT modify this file.
|
||||
*/
|
||||
|
||||
const PACKAGE_VERSION = '2.14.7'
|
||||
const INTEGRITY_CHECKSUM = '4db4a41e972cec1b64cc569c66952d82'
|
||||
const PACKAGE_VERSION = '2.15.0'
|
||||
const INTEGRITY_CHECKSUM = '03cb67ac84128e63d7cd722a6e5b7f1e'
|
||||
const IS_MOCKED_RESPONSE = Symbol('isMockedResponse')
|
||||
const activeClientIds = new Set()
|
||||
|
||||
@@ -137,8 +137,18 @@ async function handleRequest(event, requestId, requestInterceptedAt) {
|
||||
if (client && activeClientIds.has(client.id)) {
|
||||
const serializedRequest = await serializeRequest(requestCloneForEvents)
|
||||
|
||||
// Omit the body of server-sent event stream responses.
|
||||
// Cloning such responses would prevent client-side stream cancelations
|
||||
// from reaching the original stream (a teed stream only cancels its
|
||||
// source once both of its branches cancel) and would buffer the
|
||||
// entire stream into the unconsumed clone indefinitely.
|
||||
const isEventStreamResponse = response.headers
|
||||
.get('content-type')
|
||||
?.toLowerCase()
|
||||
.startsWith('text/event-stream')
|
||||
|
||||
// Clone the response so both the client and the library could consume it.
|
||||
const responseClone = response.clone()
|
||||
const responseClone = isEventStreamResponse ? null : response.clone()
|
||||
|
||||
sendToClient(
|
||||
client,
|
||||
@@ -151,15 +161,17 @@ async function handleRequest(event, requestId, requestInterceptedAt) {
|
||||
...serializedRequest,
|
||||
},
|
||||
response: {
|
||||
type: responseClone.type,
|
||||
status: responseClone.status,
|
||||
statusText: responseClone.statusText,
|
||||
headers: Object.fromEntries(responseClone.headers.entries()),
|
||||
body: responseClone.body,
|
||||
type: response.type,
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
headers: Object.fromEntries(response.headers.entries()),
|
||||
body: responseClone ? responseClone.body : null,
|
||||
},
|
||||
},
|
||||
},
|
||||
responseClone.body ? [serializedRequest.body, responseClone.body] : [],
|
||||
responseClone && responseClone.body
|
||||
? [serializedRequest.body, responseClone.body]
|
||||
: [],
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+1546
-18
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user