mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-08-26 21:17:14 +00:00
Compare commits
133 Commits
v3.6.0
...
fcf60eb2e2
| Author | SHA1 | Date | |
|---|---|---|---|
| fcf60eb2e2 | |||
| 103b0dfe8d | |||
| 2d30ab3ada | |||
| d175050f2e | |||
| 7a595cb46d | |||
| f13baa9af5 | |||
| 9408424959 | |||
| f204997c98 | |||
| effcccceac | |||
| d9b599b9aa | |||
| cc245a908e | |||
| c26ff59b47 | |||
| 6f7a305239 | |||
| da01b7637d | |||
| 81fcacab11 | |||
| 02002dc1c3 | |||
| 326009e9d3 | |||
| 585f4ecdc0 | |||
| bd6a6aba43 | |||
| a3e617215c | |||
| a255ab7c65 | |||
| af3e6c11b6 | |||
| b73ceae081 | |||
| 1250fbb734 | |||
| 5321665d5b | |||
| 73a971c2d1 | |||
| 19a2c23c01 | |||
| e4798a027c | |||
| 845abc380e | |||
| 58669f6146 | |||
| 19e71d9acc | |||
| f7db247b07 | |||
| c8a3a2d723 | |||
| b51f09768b | |||
| 3c087f6fd9 | |||
| ce63bf3e66 | |||
| b9eda09da9 | |||
| 92fb94d856 | |||
| 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 |
@@ -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
|
||||
|
||||
+537
-581
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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
+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
+2312
-3618
File diff suppressed because it is too large
Load Diff
+41
-39
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "3x-ui-frontend",
|
||||
"private": true,
|
||||
"version": "0.6.0",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"description": "3x-ui panel frontend (React 19 + Ant Design 6 + Vite 8).",
|
||||
"engines": {
|
||||
@@ -12,7 +12,11 @@
|
||||
"dev": "vite",
|
||||
"build": "npm run gen:api && vite build",
|
||||
"preview": "vite preview",
|
||||
"lint": "eslint src",
|
||||
"lint": "oxlint src tools",
|
||||
"lint:fix": "oxlint --fix src tools",
|
||||
"lint:deprecated": "oxlint --type-aware -A all -D typescript/no-deprecated src",
|
||||
"format": "oxfmt src tools",
|
||||
"format:check": "oxfmt --check src tools",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
@@ -24,64 +28,61 @@
|
||||
"prepare": "cd .. && husky frontend/.husky || true"
|
||||
},
|
||||
"lint-staged": {
|
||||
"src/**/*.{ts,tsx}": "eslint --fix"
|
||||
"src/**/*.{ts,tsx}": [
|
||||
"oxfmt",
|
||||
"oxlint --fix"
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^6.3.2",
|
||||
"@codemirror/lang-json": "^6.0.2",
|
||||
"@codemirror/theme-one-dark": "^6.1.3",
|
||||
"@hookform/resolvers": "^5.5.7",
|
||||
"@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.5",
|
||||
"@storybook/addon-docs": "^10.5.5",
|
||||
"@storybook/addon-vitest": "^10.5.5",
|
||||
"@storybook/react-vite": "^10.5.5",
|
||||
"@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.8.0",
|
||||
"@vitejs/plugin-react": "^6.1.0",
|
||||
"@vitest/browser-playwright": "4.1.11",
|
||||
"@vitest/coverage-v8": "^4.1.11",
|
||||
"husky": "^9.1.7",
|
||||
"jsdom": "^30.0.1",
|
||||
"lint-staged": "^17.2.0",
|
||||
"lint-staged": "^17.3.0",
|
||||
"msw": "^2.15.0",
|
||||
"playwright": "^1.62.0",
|
||||
"storybook": "^10.5.5",
|
||||
"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]
|
||||
: [],
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+1538
-16
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,14 @@
|
||||
(() => {
|
||||
if (!('serviceWorker' in navigator)) return;
|
||||
|
||||
const script = document.currentScript;
|
||||
if (!(script instanceof HTMLScriptElement)) return;
|
||||
|
||||
const scriptUrl = new URL(script.src, window.location.href);
|
||||
const baseUrl = new URL('./', scriptUrl);
|
||||
const workerUrl = new URL('service-worker.js', baseUrl);
|
||||
|
||||
navigator.serviceWorker.register(workerUrl.pathname, {
|
||||
scope: baseUrl.pathname,
|
||||
}).catch(() => {});
|
||||
})();
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user