mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-08-28 05:57:19 +00:00
Compare commits
212 Commits
v3.5.0
...
5321665d5b
| Author | SHA1 | Date | |
|---|---|---|---|
| 5321665d5b | |||
| 73a971c2d1 | |||
| 19a2c23c01 | |||
| e4798a027c | |||
| 845abc380e | |||
| 58669f6146 | |||
| 19e71d9acc | |||
| f7db247b07 | |||
| c8a3a2d723 | |||
| b51f09768b | |||
| 3c087f6fd9 | |||
| ce63bf3e66 | |||
| b9eda09da9 | |||
| 92fb94d856 | |||
| 380aff4d82 | |||
| 3a2f9b48da | |||
| 3f1dd4bf5a | |||
| abd320994a | |||
| 708a69acde | |||
| f75ea08ab4 | |||
| 5c7ca5b579 | |||
| b8903fadf4 | |||
| 1872659d83 | |||
| 6638ac4a1e | |||
| d6472740dc | |||
| 6e80a468e3 | |||
| e940f30bb8 | |||
| 6a674c7f0c | |||
| 81cfd8570e | |||
| 5c9268c431 | |||
| 2b1fe1fd02 | |||
| dc1979a14c | |||
| 8cec47a8a5 | |||
| 4b0e9f9b60 | |||
| 5d6d98d1f9 | |||
| b53a5515d6 | |||
| 3fa88adbd7 | |||
| 930a0ed59d | |||
| f22df49a71 | |||
| b4e4478699 | |||
| bab39393f1 | |||
| 338822ab07 | |||
| 43bc915397 | |||
| dafd3c0e64 | |||
| acbf09e710 | |||
| be70535b94 | |||
| 2d669fa4b7 | |||
| 8c8556ab32 | |||
| 03950b1295 | |||
| d7698ec7aa | |||
| 7c8a9a6909 | |||
| 694ad6deae | |||
| 1793a9b8b4 | |||
| 8e7fb144ee | |||
| 0f14ce7551 | |||
| 7ecd88b9e3 | |||
| 1230559e69 | |||
| aecbad3ab1 | |||
| 2217213e9f | |||
| ad32144c42 | |||
| 34c248bb79 | |||
| 17fea2f656 | |||
| c5dec64d36 | |||
| b56b087254 | |||
| 3bb87e80aa | |||
| 60453bf523 | |||
| bb29b6afec | |||
| 3b19091547 | |||
| 0496c23a26 | |||
| 1396005082 | |||
| b70c5abce8 | |||
| 20b3f84f77 | |||
| d291e1c5ee | |||
| ecadfd0e60 | |||
| d05e44e401 | |||
| 9165ab67eb | |||
| 0a30a03cb7 | |||
| 286a93474d | |||
| 238e4bb314 | |||
| 4a5f6771b3 | |||
| 64f4f0746c | |||
| 79ef85b59f | |||
| 5b80d4562d | |||
| e2f75acad2 | |||
| 69a8237581 | |||
| f3f57e66f5 | |||
| 8a8da88548 | |||
| 1f846c3cb2 | |||
| 1c255fc00c | |||
| 75032fd498 | |||
| ece1655939 | |||
| cb902314db | |||
| 7eacce6a46 | |||
| 199ddaf485 | |||
| d142307366 | |||
| 3883882726 | |||
| 216d18b3c4 | |||
| 2a8c3bc0db | |||
| e71b75e99e | |||
| 5bc81dfd1d | |||
| f4b7b08e08 | |||
| 1ff90c5b66 | |||
| 138e1bd840 | |||
| 31c1eed5dc | |||
| 264f61eb90 | |||
| ac584cfc90 | |||
| 91c5d7b19f | |||
| b2fe233108 | |||
| 5373786faa | |||
| c377dca27c | |||
| c56f6447a8 | |||
| 66740b7ef4 | |||
| 8d02ae28f5 | |||
| 2c943da3e0 | |||
| f52c3c4837 | |||
| 1e2d6f6081 | |||
| af5a8e5d40 | |||
| ad288a7ecc | |||
| ad5f2a28cb | |||
| e467b25f03 | |||
| 03cc80bb9e | |||
| 863473783d | |||
| c3fa73d5a0 | |||
| 87ebcc7a6f | |||
| 55f0281692 | |||
| bcd71c9296 | |||
| 33f72f8f4a | |||
| ea35884390 | |||
| 17e6b5a460 | |||
| 34d2591e50 | |||
| ca6955d88b | |||
| 411271b454 | |||
| e862d81c60 | |||
| 6af2995930 | |||
| 041476a317 | |||
| ff954ec48c | |||
| 8f49327efb | |||
| 8bbca76bdd | |||
| a2774bf212 | |||
| 48675ff197 | |||
| 604986598f | |||
| b6473004ac | |||
| 579acbc669 | |||
| 4605f00a15 | |||
| dc6a16019e | |||
| fea6a20f7c | |||
| 7f7b7e16a4 | |||
| fd17255f1d | |||
| 8bc00d1e90 | |||
| 6f4cc1e53c | |||
| 0e69f64e56 | |||
| 7fe9932d7b | |||
| c004c18d90 | |||
| f8e9f2f087 | |||
| 5accd8a611 | |||
| f46b1726cf | |||
| acbb879f80 | |||
| 1358f65bec | |||
| f4e79e70ea | |||
| edb487a005 | |||
| 35cf6be6f9 | |||
| 0f7329c3ce | |||
| 29557e2153 | |||
| 0b60154383 | |||
| c3967e57dc | |||
| aa60d54ea5 | |||
| a652cb8cea | |||
| cd674c8d4f | |||
| b319dd0c3a | |||
| 8ef2eec3d1 | |||
| 941c6116a9 | |||
| c77608bc47 | |||
| 892c06c8bc | |||
| 16b9b3ce1c | |||
| 2b1308ca29 | |||
| 9e117bbdd3 | |||
| 8cd71e07ea | |||
| 79e65f63df | |||
| c87649d9f2 | |||
| 123fac222b | |||
| d38c912dc1 | |||
| fde66ba820 | |||
| a0dec000b2 | |||
| 11f602fe04 | |||
| c80e5e276b | |||
| 22ff07b24b | |||
| d623410cf4 | |||
| a9d5d9afdb | |||
| 16b2bcf9aa | |||
| 2f156c8eb0 | |||
| 455d1cd0f7 | |||
| ab1a922806 | |||
| 5e1cb7693b | |||
| 28b360f2af | |||
| 4600771167 | |||
| b97504385f | |||
| 8dfb639dcf | |||
| 444e1e5917 | |||
| 73b479e5a0 | |||
| 129f50d92a | |||
| f2b17397f4 | |||
| 658e6ab3d3 | |||
| 1cfd7b49b0 | |||
| ae0da4c51f | |||
| 65b5074b60 | |||
| b18c87dc4b | |||
| b11ceac18e | |||
| bbc4163768 | |||
| ee9a6067c2 | |||
| 60316c831f | |||
| df3ba568d1 | |||
| 7078abc14a |
@@ -1,7 +1,7 @@
|
|||||||
name: Bug report
|
name: Bug report
|
||||||
description: Report something that is broken or behaving unexpectedly
|
description: Report something that is broken or behaving unexpectedly
|
||||||
title: "[Bug]: "
|
title: "[Bug]: "
|
||||||
labels: ["bug", "needs triage"]
|
labels: ["bug"]
|
||||||
|
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
@@ -64,7 +64,10 @@ body:
|
|||||||
id: screenshots
|
id: screenshots
|
||||||
attributes:
|
attributes:
|
||||||
label: Screenshots
|
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:
|
validations:
|
||||||
required: false
|
required: false
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
name: Feature request
|
name: Feature request
|
||||||
description: Suggest an idea or improvement for 3x-ui
|
description: Suggest an idea or improvement for 3x-ui
|
||||||
title: "[Feature]: "
|
title: "[Feature]: "
|
||||||
labels: ["enhancement", "needs triage"]
|
labels: ["enhancement"]
|
||||||
|
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
|
|||||||
@@ -73,7 +73,10 @@ body:
|
|||||||
id: screenshots
|
id: screenshots
|
||||||
attributes:
|
attributes:
|
||||||
label: Screenshots or config snippets
|
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:
|
validations:
|
||||||
required: false
|
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.26, 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`.
|
||||||
+47
-13
@@ -8,6 +8,8 @@ on:
|
|||||||
- "go.sum"
|
- "go.sum"
|
||||||
- "frontend/**"
|
- "frontend/**"
|
||||||
- ".nvmrc"
|
- ".nvmrc"
|
||||||
|
- "Makefile"
|
||||||
|
- ".github/workflows/ci.yml"
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
@@ -17,6 +19,8 @@ on:
|
|||||||
- "go.sum"
|
- "go.sum"
|
||||||
- "frontend/**"
|
- "frontend/**"
|
||||||
- ".nvmrc"
|
- ".nvmrc"
|
||||||
|
- "Makefile"
|
||||||
|
- ".github/workflows/ci.yml"
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
@@ -26,7 +30,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v7
|
||||||
- uses: actions/setup-go@v6
|
- uses: actions/setup-go@v7
|
||||||
with:
|
with:
|
||||||
go-version-file: go.mod
|
go-version-file: go.mod
|
||||||
cache: true
|
cache: true
|
||||||
@@ -53,9 +57,12 @@ jobs:
|
|||||||
--health-interval 10s
|
--health-interval 10s
|
||||||
--health-timeout 5s
|
--health-timeout 5s
|
||||||
--health-retries 5
|
--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:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v7
|
||||||
- uses: actions/setup-go@v6
|
- uses: actions/setup-go@v7
|
||||||
with:
|
with:
|
||||||
go-version-file: go.mod
|
go-version-file: go.mod
|
||||||
cache: true
|
cache: true
|
||||||
@@ -64,9 +71,24 @@ jobs:
|
|||||||
- name: PostgreSQL durable-first tests
|
- name: PostgreSQL durable-first tests
|
||||||
run: |
|
run: |
|
||||||
set -o pipefail
|
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
|
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
|
# 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
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -74,11 +96,11 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v7
|
||||||
- uses: actions/setup-go@v6
|
- uses: actions/setup-go@v7
|
||||||
with:
|
with:
|
||||||
go-version-file: go.mod
|
go-version-file: go.mod
|
||||||
cache: true
|
cache: true
|
||||||
- uses: actions/setup-node@v6
|
- uses: actions/setup-node@v7
|
||||||
with:
|
with:
|
||||||
node-version-file: .nvmrc
|
node-version-file: .nvmrc
|
||||||
- name: Regenerate schemas, examples and OpenAPI
|
- name: Regenerate schemas, examples and OpenAPI
|
||||||
@@ -91,7 +113,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v7
|
||||||
- uses: actions/setup-go@v6
|
- uses: actions/setup-go@v7
|
||||||
with:
|
with:
|
||||||
go-version-file: go.mod
|
go-version-file: go.mod
|
||||||
cache: true
|
cache: true
|
||||||
@@ -107,7 +129,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v7
|
||||||
- uses: actions/setup-go@v6
|
- uses: actions/setup-go@v7
|
||||||
with:
|
with:
|
||||||
go-version-file: go.mod
|
go-version-file: go.mod
|
||||||
cache: true
|
cache: true
|
||||||
@@ -124,7 +146,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v7
|
||||||
- uses: actions/setup-go@v6
|
- uses: actions/setup-go@v7
|
||||||
with:
|
with:
|
||||||
go-version-file: go.mod
|
go-version-file: go.mod
|
||||||
cache: true
|
cache: true
|
||||||
@@ -139,7 +161,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v7
|
||||||
- uses: actions/setup-go@v6
|
- uses: actions/setup-go@v7
|
||||||
with:
|
with:
|
||||||
go-version-file: go.mod
|
go-version-file: go.mod
|
||||||
cache: true
|
cache: true
|
||||||
@@ -154,7 +176,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v7
|
||||||
- uses: actions/setup-node@v6
|
- uses: actions/setup-node@v7
|
||||||
with:
|
with:
|
||||||
node-version-file: .nvmrc
|
node-version-file: .nvmrc
|
||||||
cache: npm
|
cache: npm
|
||||||
@@ -162,18 +184,30 @@ jobs:
|
|||||||
- name: Install
|
- name: Install
|
||||||
run: npm ci
|
run: npm ci
|
||||||
working-directory: frontend
|
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
|
- name: Lint
|
||||||
run: npm run lint
|
run: npm run lint
|
||||||
working-directory: frontend
|
working-directory: frontend
|
||||||
|
- name: Format check
|
||||||
|
run: npm run format:check
|
||||||
|
working-directory: frontend
|
||||||
- name: Typecheck
|
- name: Typecheck
|
||||||
run: npm run typecheck
|
run: npm run typecheck
|
||||||
working-directory: frontend
|
working-directory: frontend
|
||||||
|
- name: Install Playwright Chromium (Storybook story tests)
|
||||||
|
run: npx playwright install --with-deps chromium
|
||||||
|
working-directory: frontend
|
||||||
- name: Test
|
- name: Test
|
||||||
run: npm test
|
run: npm test
|
||||||
working-directory: frontend
|
working-directory: frontend
|
||||||
- name: Build
|
- name: Build
|
||||||
run: npm run build
|
run: npm run build
|
||||||
working-directory: frontend
|
working-directory: frontend
|
||||||
- name: Audit
|
- name: Build Storybook
|
||||||
run: npm audit --audit-level=high
|
run: npm run build-storybook
|
||||||
|
working-directory: frontend
|
||||||
|
- name: Audit
|
||||||
|
run: npm audit --omit=dev --audit-level=high
|
||||||
working-directory: frontend
|
working-directory: frontend
|
||||||
|
|||||||
+848
-699
File diff suppressed because it is too large
Load Diff
@@ -49,7 +49,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
if: matrix.language == 'go'
|
if: matrix.language == 'go'
|
||||||
uses: actions/setup-node@v6
|
uses: actions/setup-node@v7
|
||||||
with:
|
with:
|
||||||
node-version-file: .nvmrc
|
node-version-file: .nvmrc
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
|
|||||||
@@ -55,6 +55,8 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
push: true
|
push: true
|
||||||
|
provenance: mode=max
|
||||||
|
sbom: true
|
||||||
platforms: linux/amd64,linux/arm64/v8,linux/arm/v7,linux/arm/v6,linux/386
|
platforms: linux/amd64,linux/arm64/v8,linux/arm/v7,linux/arm/v6,linux/386
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
|||||||
@@ -26,11 +26,11 @@ jobs:
|
|||||||
|
|
||||||
- uses: pnpm/action-setup@v6
|
- uses: pnpm/action-setup@v6
|
||||||
with:
|
with:
|
||||||
version: 11
|
package_json_file: docs/package.json
|
||||||
|
|
||||||
- uses: actions/setup-node@v6
|
- uses: actions/setup-node@v7
|
||||||
with:
|
with:
|
||||||
node-version: 22
|
node-version-file: .nvmrc
|
||||||
cache: pnpm
|
cache: pnpm
|
||||||
cache-dependency-path: docs/pnpm-lock.yaml
|
cache-dependency-path: docs/pnpm-lock.yaml
|
||||||
|
|
||||||
@@ -42,6 +42,9 @@ jobs:
|
|||||||
- name: Lint
|
- name: Lint
|
||||||
run: pnpm lint
|
run: pnpm lint
|
||||||
|
|
||||||
|
- name: Format check
|
||||||
|
run: pnpm format:check
|
||||||
|
|
||||||
- name: Test
|
- name: Test
|
||||||
run: pnpm test
|
run: pnpm test
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,9 @@ on:
|
|||||||
branches: [main]
|
branches: [main]
|
||||||
paths:
|
paths:
|
||||||
- 'docs/**'
|
- 'docs/**'
|
||||||
|
- 'frontend/src/components/**'
|
||||||
|
- 'frontend/.storybook/**'
|
||||||
|
- 'frontend/package-lock.json'
|
||||||
- '.github/workflows/docs-deploy.yml'
|
- '.github/workflows/docs-deploy.yml'
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
@@ -32,10 +35,10 @@ jobs:
|
|||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v7
|
||||||
- uses: pnpm/action-setup@v6
|
- uses: pnpm/action-setup@v6
|
||||||
with:
|
with:
|
||||||
version: 11
|
package_json_file: docs/package.json
|
||||||
- uses: actions/setup-node@v6
|
- uses: actions/setup-node@v7
|
||||||
with:
|
with:
|
||||||
node-version: 22
|
node-version-file: .nvmrc
|
||||||
cache: pnpm
|
cache: pnpm
|
||||||
cache-dependency-path: docs/pnpm-lock.yaml
|
cache-dependency-path: docs/pnpm-lock.yaml
|
||||||
- run: pnpm install --frozen-lockfile
|
- run: pnpm install --frozen-lockfile
|
||||||
@@ -52,6 +55,13 @@ jobs:
|
|||||||
# in place. That way both /docs/... and /en/docs/... resolve. Other
|
# in place. That way both /docs/... and /en/docs/... resolve. Other
|
||||||
# locales stay under /fa, /ru, /zh.
|
# locales stay under /fa, /ru, /zh.
|
||||||
run: cp -a out/en/. out/
|
run: cp -a out/en/. out/
|
||||||
|
- name: Build the component Storybook (frontend/)
|
||||||
|
working-directory: frontend
|
||||||
|
run: |
|
||||||
|
npm ci
|
||||||
|
npm run build-storybook
|
||||||
|
- name: Bundle Storybook at /storybook
|
||||||
|
run: cp -a ../frontend/storybook-static out/storybook
|
||||||
- uses: actions/upload-pages-artifact@v5
|
- uses: actions/upload-pages-artifact@v5
|
||||||
with:
|
with:
|
||||||
path: docs/out
|
path: docs/out
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ jobs:
|
|||||||
exclude: 'server\.go|xray\.go|inbound\.go|client_bulk\.go|inbound_traffic\.go|.*_postgres_test\.go'
|
exclude: 'server\.go|xray\.go|inbound\.go|client_bulk\.go|inbound_traffic\.go|.*_postgres_test\.go'
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v7
|
||||||
- uses: actions/setup-go@v6
|
- uses: actions/setup-go@v7
|
||||||
with:
|
with:
|
||||||
go-version-file: go.mod
|
go-version-file: go.mod
|
||||||
cache: true
|
cache: true
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ on:
|
|||||||
- "x-ui.service.debian"
|
- "x-ui.service.debian"
|
||||||
- "x-ui.service.arch"
|
- "x-ui.service.arch"
|
||||||
- "x-ui.service.rhel"
|
- "x-ui.service.rhel"
|
||||||
|
- ".github/workflows/release.yml"
|
||||||
pull_request:
|
pull_request:
|
||||||
paths:
|
paths:
|
||||||
- "**.go"
|
- "**.go"
|
||||||
@@ -26,12 +27,15 @@ on:
|
|||||||
- "x-ui.service.debian"
|
- "x-ui.service.debian"
|
||||||
- "x-ui.service.arch"
|
- "x-ui.service.arch"
|
||||||
- "x-ui.service.rhel"
|
- "x-ui.service.rhel"
|
||||||
|
- ".github/workflows/release.yml"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
strategy:
|
strategy:
|
||||||
|
# One platform hitting a transient outage must not cancel the other six.
|
||||||
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
platform:
|
platform:
|
||||||
- amd64
|
- amd64
|
||||||
@@ -47,7 +51,7 @@ jobs:
|
|||||||
uses: actions/checkout@v7
|
uses: actions/checkout@v7
|
||||||
|
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v6
|
uses: actions/setup-go@v7
|
||||||
with:
|
with:
|
||||||
go-version-file: go.mod
|
go-version-file: go.mod
|
||||||
check-latest: true
|
check-latest: true
|
||||||
@@ -57,7 +61,7 @@ jobs:
|
|||||||
# at compile time. internal/web/dist/ is .gitignored, so on a fresh CI
|
# at compile time. internal/web/dist/ is .gitignored, so on a fresh CI
|
||||||
# checkout it doesn't exist until vite emits it.
|
# checkout it doesn't exist until vite emits it.
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v6
|
uses: actions/setup-node@v7
|
||||||
with:
|
with:
|
||||||
node-version-file: .nvmrc
|
node-version-file: .nvmrc
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
@@ -71,6 +75,8 @@ jobs:
|
|||||||
|
|
||||||
- name: Build 3X-UI
|
- name: Build 3X-UI
|
||||||
run: |
|
run: |
|
||||||
|
CURL_RETRY="--retry 5 --retry-all-errors --retry-delay 3"
|
||||||
|
fetch() { wget -q --tries=5 --waitretry=10 --retry-on-http-error=429,500,502,503 "$@"; }
|
||||||
export CGO_ENABLED=1
|
export CGO_ENABLED=1
|
||||||
export GOOS=linux
|
export GOOS=linux
|
||||||
export GOARCH=${{ matrix.platform }}
|
export GOARCH=${{ matrix.platform }}
|
||||||
@@ -86,11 +92,11 @@ jobs:
|
|||||||
esac
|
esac
|
||||||
echo "Resolving Bootlin musl toolchain for arch=$BOOTLIN_ARCH (platform=${{ matrix.platform }})"
|
echo "Resolving Bootlin musl toolchain for arch=$BOOTLIN_ARCH (platform=${{ matrix.platform }})"
|
||||||
TARBALL_BASE="https://toolchains.bootlin.com/downloads/releases/toolchains/$BOOTLIN_ARCH/tarballs/"
|
TARBALL_BASE="https://toolchains.bootlin.com/downloads/releases/toolchains/$BOOTLIN_ARCH/tarballs/"
|
||||||
TARBALL_URL=$(curl -fsSL "$TARBALL_BASE" | grep -oE "${BOOTLIN_ARCH}--musl--stable-[^\"]+\\.tar\\.xz" | sort -r | head -n1)
|
TARBALL_URL=$(curl -fsSL $CURL_RETRY "$TARBALL_BASE" | grep -oE "${BOOTLIN_ARCH}--musl--stable-[^\"]+\\.tar\\.xz" | sort -r | head -n1)
|
||||||
[ -z "$TARBALL_URL" ] && { echo "Failed to locate Bootlin musl toolchain for arch=$BOOTLIN_ARCH" >&2; exit 1; }
|
[ -z "$TARBALL_URL" ] && { echo "Failed to locate Bootlin musl toolchain for arch=$BOOTLIN_ARCH" >&2; exit 1; }
|
||||||
echo "Downloading: $TARBALL_URL"
|
echo "Downloading: $TARBALL_URL"
|
||||||
cd /tmp
|
cd /tmp
|
||||||
curl -fL -sS -o "$(basename "$TARBALL_URL")" "$TARBALL_BASE/$TARBALL_URL"
|
curl -fL -sS $CURL_RETRY -o "$(basename "$TARBALL_URL")" "$TARBALL_BASE/$TARBALL_URL"
|
||||||
tar -xf "$(basename "$TARBALL_URL")"
|
tar -xf "$(basename "$TARBALL_URL")"
|
||||||
TOOLCHAIN_DIR=$(find . -maxdepth 1 -type d -name "${BOOTLIN_ARCH}--musl--stable-*" | head -n1)
|
TOOLCHAIN_DIR=$(find . -maxdepth 1 -type d -name "${BOOTLIN_ARCH}--musl--stable-*" | head -n1)
|
||||||
export PATH="$(realpath "$TOOLCHAIN_DIR")/bin:$PATH"
|
export PATH="$(realpath "$TOOLCHAIN_DIR")/bin:$PATH"
|
||||||
@@ -103,7 +109,7 @@ jobs:
|
|||||||
if [[ "$GITHUB_REF" != refs/tags/* ]]; then
|
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)"
|
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
|
fi
|
||||||
go build -ldflags "$LDFLAGS" -o xui-release -v main.go
|
go build -buildvcs=true -ldflags "$LDFLAGS" -o xui-release -v .
|
||||||
file xui-release
|
file xui-release
|
||||||
ldd xui-release || echo "Static binary confirmed"
|
ldd xui-release || echo "Static binary confirmed"
|
||||||
|
|
||||||
@@ -118,55 +124,57 @@ jobs:
|
|||||||
cd x-ui/bin
|
cd x-ui/bin
|
||||||
|
|
||||||
# Download dependencies
|
# Download dependencies
|
||||||
Xray_URL="https://github.com/XTLS/Xray-core/releases/download/v26.7.11/"
|
Xray_URL="https://github.com/XTLS/Xray-core/releases/download/v26.7.28/"
|
||||||
if [ "${{ matrix.platform }}" == "amd64" ]; then
|
if [ "${{ matrix.platform }}" == "amd64" ]; then
|
||||||
wget -q ${Xray_URL}Xray-linux-64.zip
|
fetch ${Xray_URL}Xray-linux-64.zip
|
||||||
unzip Xray-linux-64.zip
|
unzip Xray-linux-64.zip
|
||||||
rm -f Xray-linux-64.zip
|
rm -f Xray-linux-64.zip
|
||||||
elif [ "${{ matrix.platform }}" == "arm64" ]; then
|
elif [ "${{ matrix.platform }}" == "arm64" ]; then
|
||||||
wget -q ${Xray_URL}Xray-linux-arm64-v8a.zip
|
fetch ${Xray_URL}Xray-linux-arm64-v8a.zip
|
||||||
unzip Xray-linux-arm64-v8a.zip
|
unzip Xray-linux-arm64-v8a.zip
|
||||||
rm -f Xray-linux-arm64-v8a.zip
|
rm -f Xray-linux-arm64-v8a.zip
|
||||||
elif [ "${{ matrix.platform }}" == "armv7" ]; then
|
elif [ "${{ matrix.platform }}" == "armv7" ]; then
|
||||||
wget -q ${Xray_URL}Xray-linux-arm32-v7a.zip
|
fetch ${Xray_URL}Xray-linux-arm32-v7a.zip
|
||||||
unzip Xray-linux-arm32-v7a.zip
|
unzip Xray-linux-arm32-v7a.zip
|
||||||
rm -f Xray-linux-arm32-v7a.zip
|
rm -f Xray-linux-arm32-v7a.zip
|
||||||
elif [ "${{ matrix.platform }}" == "armv6" ]; then
|
elif [ "${{ matrix.platform }}" == "armv6" ]; then
|
||||||
wget -q ${Xray_URL}Xray-linux-arm32-v6.zip
|
fetch ${Xray_URL}Xray-linux-arm32-v6.zip
|
||||||
unzip Xray-linux-arm32-v6.zip
|
unzip Xray-linux-arm32-v6.zip
|
||||||
rm -f Xray-linux-arm32-v6.zip
|
rm -f Xray-linux-arm32-v6.zip
|
||||||
elif [ "${{ matrix.platform }}" == "386" ]; then
|
elif [ "${{ matrix.platform }}" == "386" ]; then
|
||||||
wget -q ${Xray_URL}Xray-linux-32.zip
|
fetch ${Xray_URL}Xray-linux-32.zip
|
||||||
unzip Xray-linux-32.zip
|
unzip Xray-linux-32.zip
|
||||||
rm -f Xray-linux-32.zip
|
rm -f Xray-linux-32.zip
|
||||||
elif [ "${{ matrix.platform }}" == "armv5" ]; then
|
elif [ "${{ matrix.platform }}" == "armv5" ]; then
|
||||||
wget -q ${Xray_URL}Xray-linux-arm32-v5.zip
|
fetch ${Xray_URL}Xray-linux-arm32-v5.zip
|
||||||
unzip Xray-linux-arm32-v5.zip
|
unzip Xray-linux-arm32-v5.zip
|
||||||
rm -f Xray-linux-arm32-v5.zip
|
rm -f Xray-linux-arm32-v5.zip
|
||||||
elif [ "${{ matrix.platform }}" == "s390x" ]; then
|
elif [ "${{ matrix.platform }}" == "s390x" ]; then
|
||||||
wget -q ${Xray_URL}Xray-linux-s390x.zip
|
fetch ${Xray_URL}Xray-linux-s390x.zip
|
||||||
unzip Xray-linux-s390x.zip
|
unzip Xray-linux-s390x.zip
|
||||||
rm -f Xray-linux-s390x.zip
|
rm -f Xray-linux-s390x.zip
|
||||||
fi
|
fi
|
||||||
rm -f geoip.dat geosite.dat
|
rm -f geoip.dat geosite.dat
|
||||||
wget -q https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat
|
fetch https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat
|
||||||
wget -q https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat
|
fetch https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat
|
||||||
wget -q -O geoip_IR.dat https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geoip.dat
|
fetch -O geoip_IR.dat https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geoip.dat
|
||||||
wget -q -O geosite_IR.dat https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geosite.dat
|
fetch -O geosite_IR.dat https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geosite.dat
|
||||||
wget -q -O geoip_RU.dat https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geoip.dat
|
fetch -O geoip_RU.dat https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geoip.dat
|
||||||
wget -q -O geosite_RU.dat https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geosite.dat
|
fetch -O geosite_RU.dat https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geosite.dat
|
||||||
mv xray xray-linux-${{ matrix.platform }}
|
mv xray xray-linux-${{ matrix.platform }}
|
||||||
# mtg-multi (MTProto sidecar) ships prebuilt release binaries whose
|
# mtg-multi (MTProto sidecar) ships prebuilt release binaries whose
|
||||||
# platform labels match our matrix, so download and unpack the matching
|
# platform labels match our matrix, so download and unpack the matching
|
||||||
# archive. Only the platforms the fork publishes are packaged. The tag
|
# archive. Only the platforms the fork publishes are packaged — the tag
|
||||||
# is resolved from the fork's latest release so it never needs bumping
|
# lookup lives inside that branch so unpackaged platforms (s390x) never
|
||||||
# here; the token only lifts the API rate limit for a public read.
|
# depend on it. The tag comes from the release-page redirect on
|
||||||
MTG_MULTI_VER=$(curl -sfL -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" "https://api.github.com/repos/mhsanaei/mtg-multi/releases/latest" | sed -n 's/.*"tag_name": *"\([^"]*\)".*/\1/p' | head -n 1)
|
# github.com — the host the downloads need anyway — because api.github.com
|
||||||
if [ -z "$MTG_MULTI_VER" ]; then echo "could not resolve the latest mtg-multi release tag"; exit 1; fi
|
# has 503'd whole release runs while asset downloads kept working.
|
||||||
case "${{ matrix.platform }}" in
|
case "${{ matrix.platform }}" in
|
||||||
amd64|arm64|armv7|armv6|386)
|
amd64|arm64|armv7|armv6|386)
|
||||||
|
MTG_MULTI_VER=$(curl -sf $CURL_RETRY -o /dev/null -w '%{redirect_url}' "https://github.com/mhsanaei/mtg-multi/releases/latest" | sed -n 's#.*/releases/tag/##p')
|
||||||
|
if [ -z "$MTG_MULTI_VER" ]; then echo "could not resolve the latest mtg-multi release tag"; exit 1; fi
|
||||||
MTG_PKG="mtg-multi-${MTG_MULTI_VER#v}-linux-${{ matrix.platform }}"
|
MTG_PKG="mtg-multi-${MTG_MULTI_VER#v}-linux-${{ matrix.platform }}"
|
||||||
curl -sfLRO "https://github.com/mhsanaei/mtg-multi/releases/download/${MTG_MULTI_VER}/${MTG_PKG}.tar.gz"
|
curl -sfLRO $CURL_RETRY "https://github.com/mhsanaei/mtg-multi/releases/download/${MTG_MULTI_VER}/${MTG_PKG}.tar.gz"
|
||||||
tar -xzf "${MTG_PKG}.tar.gz"
|
tar -xzf "${MTG_PKG}.tar.gz"
|
||||||
mv "${MTG_PKG}/mtg-multi" "mtg-linux-${{ matrix.platform }}"
|
mv "${MTG_PKG}/mtg-multi" "mtg-linux-${{ matrix.platform }}"
|
||||||
rm -rf "${MTG_PKG}" "${MTG_PKG}.tar.gz"
|
rm -rf "${MTG_PKG}" "${MTG_PKG}.tar.gz"
|
||||||
@@ -211,7 +219,7 @@ jobs:
|
|||||||
uses: actions/checkout@v7
|
uses: actions/checkout@v7
|
||||||
|
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v6
|
uses: actions/setup-go@v7
|
||||||
with:
|
with:
|
||||||
go-version-file: go.mod
|
go-version-file: go.mod
|
||||||
check-latest: true
|
check-latest: true
|
||||||
@@ -220,7 +228,7 @@ jobs:
|
|||||||
# Linux job above. This step is identical except npm runs on the
|
# Linux job above. This step is identical except npm runs on the
|
||||||
# Windows runner here.
|
# Windows runner here.
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v6
|
uses: actions/setup-node@v7
|
||||||
with:
|
with:
|
||||||
node-version-file: .nvmrc
|
node-version-file: .nvmrc
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
@@ -239,6 +247,7 @@ jobs:
|
|||||||
msystem: MINGW64
|
msystem: MINGW64
|
||||||
update: true
|
update: true
|
||||||
install: >-
|
install: >-
|
||||||
|
git
|
||||||
mingw-w64-x86_64-gcc
|
mingw-w64-x86_64-gcc
|
||||||
mingw-w64-x86_64-sqlite3
|
mingw-w64-x86_64-sqlite3
|
||||||
mingw-w64-x86_64-pkg-config
|
mingw-w64-x86_64-pkg-config
|
||||||
@@ -262,37 +271,39 @@ jobs:
|
|||||||
if [[ "$GITHUB_REF" != refs/tags/* ]]; then
|
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)"
|
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
|
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
|
- name: Copy and download resources
|
||||||
shell: pwsh
|
shell: pwsh
|
||||||
run: |
|
run: |
|
||||||
|
$retry = @{ MaximumRetryCount = 5; RetryIntervalSec = 10 }
|
||||||
mkdir x-ui
|
mkdir x-ui
|
||||||
Copy-Item xui-release.exe x-ui\x-ui.exe
|
Copy-Item xui-release.exe x-ui\x-ui.exe
|
||||||
mkdir x-ui\bin
|
mkdir x-ui\bin
|
||||||
cd x-ui\bin
|
cd x-ui\bin
|
||||||
|
|
||||||
# Download Xray for Windows
|
# Download Xray for Windows
|
||||||
$Xray_URL = "https://github.com/XTLS/Xray-core/releases/download/v26.7.11/"
|
$Xray_URL = "https://github.com/XTLS/Xray-core/releases/download/v26.7.28/"
|
||||||
Invoke-WebRequest -Uri "${Xray_URL}Xray-windows-64.zip" -OutFile "Xray-windows-64.zip"
|
Invoke-WebRequest @retry -Uri "${Xray_URL}Xray-windows-64.zip" -OutFile "Xray-windows-64.zip"
|
||||||
Expand-Archive -Path "Xray-windows-64.zip" -DestinationPath .
|
Expand-Archive -Path "Xray-windows-64.zip" -DestinationPath .
|
||||||
Remove-Item "Xray-windows-64.zip"
|
Remove-Item "Xray-windows-64.zip"
|
||||||
Remove-Item geoip.dat, geosite.dat -ErrorAction SilentlyContinue
|
Remove-Item geoip.dat, geosite.dat -ErrorAction SilentlyContinue
|
||||||
Invoke-WebRequest -Uri "https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat" -OutFile "geoip.dat"
|
Invoke-WebRequest @retry -Uri "https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat" -OutFile "geoip.dat"
|
||||||
Invoke-WebRequest -Uri "https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat" -OutFile "geosite.dat"
|
Invoke-WebRequest @retry -Uri "https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat" -OutFile "geosite.dat"
|
||||||
Invoke-WebRequest -Uri "https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geoip.dat" -OutFile "geoip_IR.dat"
|
Invoke-WebRequest @retry -Uri "https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geoip.dat" -OutFile "geoip_IR.dat"
|
||||||
Invoke-WebRequest -Uri "https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geosite.dat" -OutFile "geosite_IR.dat"
|
Invoke-WebRequest @retry -Uri "https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geosite.dat" -OutFile "geosite_IR.dat"
|
||||||
Invoke-WebRequest -Uri "https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geoip.dat" -OutFile "geoip_RU.dat"
|
Invoke-WebRequest @retry -Uri "https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geoip.dat" -OutFile "geoip_RU.dat"
|
||||||
Invoke-WebRequest -Uri "https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geosite.dat" -OutFile "geosite_RU.dat"
|
Invoke-WebRequest @retry -Uri "https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geosite.dat" -OutFile "geosite_RU.dat"
|
||||||
Rename-Item xray.exe xray-windows-amd64.exe
|
Rename-Item xray.exe xray-windows-amd64.exe
|
||||||
|
|
||||||
# mtg-multi (MTProto sidecar) publishes a prebuilt Windows binary, so
|
# mtg-multi (MTProto sidecar) publishes a prebuilt Windows binary, so
|
||||||
# download and unpack it instead of compiling. The tag tracks the
|
# download and unpack it instead of compiling. The tag comes from the
|
||||||
# fork's latest release so it never needs bumping here.
|
# release-page redirect on github.com — not api.github.com, whose
|
||||||
$MTG_MULTI_VER = (Invoke-RestMethod -Uri "https://api.github.com/repos/mhsanaei/mtg-multi/releases/latest" -Headers @{ Authorization = "Bearer ${{ secrets.GITHUB_TOKEN }}"; "User-Agent" = "x-ui-release" }).tag_name
|
# outages have failed release runs while asset downloads kept working.
|
||||||
if (-not $MTG_MULTI_VER) { throw "could not resolve the latest mtg-multi release tag" }
|
$MTG_MULTI_VER = (curl.exe -sf --retry 5 --retry-all-errors --retry-delay 3 -o NUL -w '%{redirect_url}' "https://github.com/mhsanaei/mtg-multi/releases/latest") -replace '^.*/releases/tag/', ''
|
||||||
|
if (-not $MTG_MULTI_VER -or $MTG_MULTI_VER -notmatch '^v[\d.]+$') { throw "could not resolve the latest mtg-multi release tag" }
|
||||||
$MTG_PKG = "mtg-multi-$($MTG_MULTI_VER.TrimStart('v'))-windows-amd64"
|
$MTG_PKG = "mtg-multi-$($MTG_MULTI_VER.TrimStart('v'))-windows-amd64"
|
||||||
curl.exe -sfLRO "https://github.com/mhsanaei/mtg-multi/releases/download/$MTG_MULTI_VER/$MTG_PKG.zip"
|
curl.exe -sfLRO --retry 5 --retry-all-errors --retry-delay 3 "https://github.com/mhsanaei/mtg-multi/releases/download/$MTG_MULTI_VER/$MTG_PKG.zip"
|
||||||
Expand-Archive -Path "$MTG_PKG.zip" -DestinationPath "mtg-tmp" -Force
|
Expand-Archive -Path "$MTG_PKG.zip" -DestinationPath "mtg-tmp" -Force
|
||||||
Move-Item "mtg-tmp/$MTG_PKG/mtg-multi.exe" "mtg-windows-amd64.exe"
|
Move-Item "mtg-tmp/$MTG_PKG/mtg-multi.exe" "mtg-windows-amd64.exe"
|
||||||
Remove-Item -Recurse -Force "mtg-tmp", "$MTG_PKG.zip"
|
Remove-Item -Recurse -Force "mtg-tmp", "$MTG_PKG.zip"
|
||||||
@@ -359,6 +370,14 @@ jobs:
|
|||||||
COMMIT: ${{ github.sha }}
|
COMMIT: ${{ github.sha }}
|
||||||
run: |
|
run: |
|
||||||
set -e
|
set -e
|
||||||
|
retry() {
|
||||||
|
for i in 1 2 3 4 5; do
|
||||||
|
"$@" && return 0
|
||||||
|
echo "attempt $i failed: ${*:1:3}" >&2
|
||||||
|
sleep $((i * 5))
|
||||||
|
done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
short="${COMMIT::8}"
|
short="${COMMIT::8}"
|
||||||
notes="Rolling development build — installs via the panel's Dev update channel.
|
notes="Rolling development build — installs via the panel's Dev update channel.
|
||||||
|
|
||||||
@@ -369,14 +388,14 @@ jobs:
|
|||||||
|
|
||||||
# Force-move the dev-latest tag to this commit so the release tracks it.
|
# Force-move the dev-latest tag to this commit so the release tracks it.
|
||||||
git tag -f dev-latest "${COMMIT}"
|
git tag -f dev-latest "${COMMIT}"
|
||||||
git push -f origin refs/tags/dev-latest
|
retry git push -f origin refs/tags/dev-latest
|
||||||
|
|
||||||
if gh release view dev-latest >/dev/null 2>&1; then
|
# The release exists on every run but the first; edit-first avoids an
|
||||||
gh release edit dev-latest --prerelease --latest=false \
|
# existence probe that can 503 and mis-route into create (422).
|
||||||
--title "Dev build ${short}" --notes "${notes}"
|
if ! retry gh release edit dev-latest --prerelease --latest=false \
|
||||||
else
|
--title "Dev build ${short}" --notes "${notes}"; then
|
||||||
gh release create dev-latest --prerelease --latest=false \
|
retry gh release create dev-latest --prerelease --latest=false \
|
||||||
--target "${COMMIT}" --title "Dev build ${short}" --notes "${notes}"
|
--target "${COMMIT}" --title "Dev build ${short}" --notes "${notes}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
gh release upload dev-latest dev-artifacts/*.tar.gz dev-artifacts/*.zip --clobber
|
retry gh release upload dev-latest dev-artifacts/*.tar.gz dev-artifacts/*.zip --clobber
|
||||||
|
|||||||
+2
-4
@@ -2,7 +2,8 @@
|
|||||||
.idea/
|
.idea/
|
||||||
.vscode/
|
.vscode/
|
||||||
.cursor/
|
.cursor/
|
||||||
.claude/*
|
.specify/
|
||||||
|
.claude/
|
||||||
.cache/
|
.cache/
|
||||||
.sync*
|
.sync*
|
||||||
|
|
||||||
@@ -18,9 +19,6 @@ backup/
|
|||||||
bin/
|
bin/
|
||||||
x-ui/
|
x-ui/
|
||||||
dist/
|
dist/
|
||||||
!internal/web/dist/
|
|
||||||
internal/web/dist/*
|
|
||||||
!internal/web/dist/.gitkeep
|
|
||||||
release/
|
release/
|
||||||
node_modules/
|
node_modules/
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -32,7 +32,7 @@ linters:
|
|||||||
# golang.org/x/tools/go/packages is a generator change, out of scope here.
|
# golang.org/x/tools/go/packages is a generator change, out of scope here.
|
||||||
- linters:
|
- linters:
|
||||||
- staticcheck
|
- staticcheck
|
||||||
text: "SA1019: parser.ParseDir"
|
text: 'SA1019: (go/)?parser\.ParseDir'
|
||||||
# ST1005 (capitalized error strings) conflicts with intentional
|
# ST1005 (capitalized error strings) conflicts with intentional
|
||||||
# user-facing error copy that tests assert verbatim.
|
# user-facing error copy that tests assert verbatim.
|
||||||
- linters:
|
- linters:
|
||||||
|
|||||||
Vendored
+10
-1
@@ -24,6 +24,14 @@
|
|||||||
"mode": "auto",
|
"mode": "auto",
|
||||||
"program": "${workspaceFolder}",
|
"program": "${workspaceFolder}",
|
||||||
"cwd": "${workspaceFolder}",
|
"cwd": "${workspaceFolder}",
|
||||||
|
"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"
|
||||||
|
},
|
||||||
|
"windows": {
|
||||||
"env": {
|
"env": {
|
||||||
"XUI_DEBUG": "true",
|
"XUI_DEBUG": "true",
|
||||||
"XUI_LOG_FOLDER": "x-ui",
|
"XUI_LOG_FOLDER": "x-ui",
|
||||||
@@ -31,8 +39,9 @@
|
|||||||
"XUI_DB_TYPE": "postgres",
|
"XUI_DB_TYPE": "postgres",
|
||||||
"XUI_DB_DSN": "postgres://xui:xuipass@127.0.0.1:5432/xui?sslmode=disable",
|
"XUI_DB_DSN": "postgres://xui:xuipass@127.0.0.1:5432/xui?sslmode=disable",
|
||||||
"PATH": "C:\\Program Files\\PostgreSQL\\18\\bin;${env:PATH}"
|
"PATH": "C:\\Program Files\\PostgreSQL\\18\\bin;${env:PATH}"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"console": "integratedTerminal"
|
"console": "integratedTerminal"
|
||||||
},
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
Vendored
+140
-1
@@ -8,9 +8,17 @@
|
|||||||
"args": [
|
"args": [
|
||||||
"build",
|
"build",
|
||||||
"-o",
|
"-o",
|
||||||
"bin/3x-ui.exe",
|
"bin/3x-ui",
|
||||||
"./main.go"
|
"./main.go"
|
||||||
],
|
],
|
||||||
|
"windows": {
|
||||||
|
"args": [
|
||||||
|
"build",
|
||||||
|
"-o",
|
||||||
|
"bin/3x-ui.exe",
|
||||||
|
"./main.go"
|
||||||
|
]
|
||||||
|
},
|
||||||
"options": {
|
"options": {
|
||||||
"cwd": "${workspaceFolder}"
|
"cwd": "${workspaceFolder}"
|
||||||
},
|
},
|
||||||
@@ -96,6 +104,22 @@
|
|||||||
"options": {
|
"options": {
|
||||||
"cwd": "${workspaceFolder}"
|
"cwd": "${workspaceFolder}"
|
||||||
},
|
},
|
||||||
|
"linux": {
|
||||||
|
"options": {
|
||||||
|
"cwd": "${workspaceFolder}",
|
||||||
|
"env": {
|
||||||
|
"PATH": "${userHome}/go/bin:/usr/local/go/bin:${env:PATH}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"osx": {
|
||||||
|
"options": {
|
||||||
|
"cwd": "${workspaceFolder}",
|
||||||
|
"env": {
|
||||||
|
"PATH": "${userHome}/go/bin:/usr/local/go/bin:${env:PATH}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"problemMatcher": [
|
"problemMatcher": [
|
||||||
"$go"
|
"$go"
|
||||||
]
|
]
|
||||||
@@ -111,6 +135,22 @@
|
|||||||
"options": {
|
"options": {
|
||||||
"cwd": "${workspaceFolder}"
|
"cwd": "${workspaceFolder}"
|
||||||
},
|
},
|
||||||
|
"linux": {
|
||||||
|
"options": {
|
||||||
|
"cwd": "${workspaceFolder}",
|
||||||
|
"env": {
|
||||||
|
"PATH": "${userHome}/go/bin:/usr/local/go/bin:${env:PATH}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"osx": {
|
||||||
|
"options": {
|
||||||
|
"cwd": "${workspaceFolder}",
|
||||||
|
"env": {
|
||||||
|
"PATH": "${userHome}/go/bin:/usr/local/go/bin:${env:PATH}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"problemMatcher": [
|
"problemMatcher": [
|
||||||
"$go"
|
"$go"
|
||||||
]
|
]
|
||||||
@@ -125,6 +165,22 @@
|
|||||||
"options": {
|
"options": {
|
||||||
"cwd": "${workspaceFolder}"
|
"cwd": "${workspaceFolder}"
|
||||||
},
|
},
|
||||||
|
"linux": {
|
||||||
|
"options": {
|
||||||
|
"cwd": "${workspaceFolder}",
|
||||||
|
"env": {
|
||||||
|
"PATH": "${userHome}/go/bin:/usr/local/go/bin:${env:PATH}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"osx": {
|
||||||
|
"options": {
|
||||||
|
"cwd": "${workspaceFolder}",
|
||||||
|
"env": {
|
||||||
|
"PATH": "${userHome}/go/bin:/usr/local/go/bin:${env:PATH}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"problemMatcher": [
|
"problemMatcher": [
|
||||||
"$go"
|
"$go"
|
||||||
]
|
]
|
||||||
@@ -140,10 +196,93 @@
|
|||||||
"options": {
|
"options": {
|
||||||
"cwd": "${workspaceFolder}"
|
"cwd": "${workspaceFolder}"
|
||||||
},
|
},
|
||||||
|
"linux": {
|
||||||
|
"options": {
|
||||||
|
"cwd": "${workspaceFolder}",
|
||||||
|
"env": {
|
||||||
|
"PATH": "${userHome}/go/bin:/usr/local/go/bin:${env:PATH}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"osx": {
|
||||||
|
"options": {
|
||||||
|
"cwd": "${workspaceFolder}",
|
||||||
|
"env": {
|
||||||
|
"PATH": "${userHome}/go/bin:/usr/local/go/bin:${env:PATH}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"problemMatcher": [
|
"problemMatcher": [
|
||||||
"$go"
|
"$go"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"label": "go: install golangci-lint",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "go",
|
||||||
|
"args": [
|
||||||
|
"install",
|
||||||
|
"github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest"
|
||||||
|
],
|
||||||
|
"options": {
|
||||||
|
"cwd": "${workspaceFolder}"
|
||||||
|
},
|
||||||
|
"linux": {
|
||||||
|
"options": {
|
||||||
|
"cwd": "${workspaceFolder}",
|
||||||
|
"env": {
|
||||||
|
"PATH": "${userHome}/go/bin:/usr/local/go/bin:${env:PATH}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"osx": {
|
||||||
|
"options": {
|
||||||
|
"cwd": "${workspaceFolder}",
|
||||||
|
"env": {
|
||||||
|
"PATH": "${userHome}/go/bin:/usr/local/go/bin:${env:PATH}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"problemMatcher": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "go: install modernize",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "go",
|
||||||
|
"args": [
|
||||||
|
"install",
|
||||||
|
"golang.org/x/tools/gopls/internal/analysis/modernize/cmd/modernize@latest"
|
||||||
|
],
|
||||||
|
"options": {
|
||||||
|
"cwd": "${workspaceFolder}"
|
||||||
|
},
|
||||||
|
"linux": {
|
||||||
|
"options": {
|
||||||
|
"cwd": "${workspaceFolder}",
|
||||||
|
"env": {
|
||||||
|
"PATH": "${userHome}/go/bin:/usr/local/go/bin:${env:PATH}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"osx": {
|
||||||
|
"options": {
|
||||||
|
"cwd": "${workspaceFolder}",
|
||||||
|
"env": {
|
||||||
|
"PATH": "${userHome}/go/bin:/usr/local/go/bin:${env:PATH}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"problemMatcher": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "go: install tools",
|
||||||
|
"dependsOrder": "sequence",
|
||||||
|
"dependsOn": [
|
||||||
|
"go: install golangci-lint",
|
||||||
|
"go: install modernize"
|
||||||
|
],
|
||||||
|
"problemMatcher": []
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"label": "frontend: ncu -u",
|
"label": "frontend: ncu -u",
|
||||||
"type": "shell",
|
"type": "shell",
|
||||||
|
|||||||
@@ -12,8 +12,10 @@ file locations when it can answer in one hop.
|
|||||||
Runs Xray-core as a managed child process (`internal/xray/process.go`) and
|
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
|
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
|
API. MTProto inbounds run a second managed child — the `mtg-multi` binary
|
||||||
(`github.com/mhsanaei/mtg-multi`, a multi-secret fork built from source;
|
(a multi-secret mtg fork — NOT a Go dependency; its prebuilt release binary is
|
||||||
`internal/mtproto/`) — outside Xray, one process per inbound serving each
|
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
|
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
|
ad-tags via `[secret-ad-tags]` and per-client data quota / expiry via
|
||||||
`[secret-limits]`, mapped from the client's `totalGB`/`expiryTime`). Client,
|
`[secret-limits]`, mapped from the client's `totalGB`/`expiryTime`). Client,
|
||||||
@@ -32,10 +34,12 @@ file locations when it can answer in one hop.
|
|||||||
- `main.go` — entry point + `x-ui` CLI (run, migrate, migrate-db, setting, cert).
|
- `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,
|
- `internal/config/` — env parsing (XUI_DEBUG, XUI_LOG_LEVEL, XUI_LOG_FOLDER,
|
||||||
XUI_BIN_FOLDER, XUI_SKIP_HSTS, XUI_PORT, XUI_DB_*).
|
XUI_BIN_FOLDER, XUI_SKIP_HSTS, XUI_PORT, XUI_DB_*).
|
||||||
- `internal/database/` + `internal/database/model/` — GORM schema (Inbound,
|
- `internal/database/` + `internal/database/model/` — GORM schema (~24 models;
|
||||||
Client, Setting, User), inbound Protocol enum, AutoMigrate + hand-written
|
Inbound, Client, Setting, User are the core), inbound Protocol enum,
|
||||||
migrations in `db.go`.
|
AutoMigrate + hand-written migrations in `db.go`.
|
||||||
- `internal/xray/` — Xray child-process lifecycle, config generation, gRPC API.
|
- `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/mtproto/` — MTProto inbounds via the bundled `mtg-multi` binary.
|
||||||
- `internal/sub/` — subscription server (raw / JSON / Clash).
|
- `internal/sub/` — subscription server (raw / JSON / Clash).
|
||||||
- `internal/eventbus/` — in-process pub/sub (outbound/node health, xray.crash,
|
- `internal/eventbus/` — in-process pub/sub (outbound/node health, xray.crash,
|
||||||
@@ -46,7 +50,8 @@ file locations when it can answer in one hop.
|
|||||||
- `controller/` — panel + REST API handlers; OpenAPI at /panel/api/openapi.json.
|
- `controller/` — panel + REST API handlers; OpenAPI at /panel/api/openapi.json.
|
||||||
- `service/` — business logic (InboundService, SettingService, XrayService,
|
- `service/` — business logic (InboundService, SettingService, XrayService,
|
||||||
node sync); subpackages tgbot/, email/, outbound/, panel/, integration/.
|
node sync); subpackages tgbot/, email/, outbound/, panel/, integration/.
|
||||||
- `job/` — cron jobs (traffic, fail2ban IP-limit, node heartbeat/sync, LDAP).
|
- `job/` — 17 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/`,
|
- `middleware/`, `entity/`, `global/`, `session/` (CSRF), `network/`,
|
||||||
`runtime/` (master/sub-node over mTLS), `websocket/`.
|
`runtime/` (master/sub-node over mTLS), `websocket/`.
|
||||||
- `locale/` + `translation/` — i18n, 13 embedded locale JSON files.
|
- `locale/` + `translation/` — i18n, 13 embedded locale JSON files.
|
||||||
@@ -55,26 +60,49 @@ file locations when it can answer in one hop.
|
|||||||
into `frontend/src/generated/` from Go structs. The OpenAPI doc itself
|
into `frontend/src/generated/` from Go structs. The OpenAPI doc itself
|
||||||
(`frontend/public/openapi.json`) is assembled from those + `endpoints.ts` by
|
(`frontend/public/openapi.json`) is assembled from those + `endpoints.ts` by
|
||||||
`frontend/scripts/build-openapi.mjs`.
|
`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)
|
## Hard rules (non-negotiable)
|
||||||
- NO `//` line comments in committed Go/TS. Names carry meaning; rename instead
|
- Fix size must match bug size. Find the root cause, then make the SMALLEST
|
||||||
of annotating. Exempt: `//go:build`, `//go:generate`, and other directives.
|
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.)
|
HTML `<!-- -->` is fine. (A linter cannot enforce this — you must.)
|
||||||
- New `g.POST`/`g.GET` in `internal/web/controller/` REQUIRES a matching entry
|
- 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
|
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
|
`cd frontend && npm run gen`). Hand-maintained but pinned both ways by
|
||||||
it against the Go routes, so an omitted route silently vanishes from the docs.
|
`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` —
|
- 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`
|
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
|
allowlist (`tools/openapigen/main.go`) or it is silently omitted from
|
||||||
schemas/examples (and `build-openapi.mjs` then fails on the missing schema).
|
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
|
- A new or renamed endpoint has a FOURTH step nothing checks: copy
|
||||||
`internal/web/translation/` (13 files). Missing keys fall back to en-US (or
|
`frontend/public/openapi.json` → `docs/public/openapi.json`, then
|
||||||
render the raw key if absent there too); nothing fails the build, so they are
|
`cd docs && pnpm gen:api` to refresh the MDX under
|
||||||
easy to miss.
|
`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`.
|
- DB / model changes require a migration in `internal/database/db.go`.
|
||||||
- Conventional-commit prefixes (`feat`, `fix`, `refactor`, `chore`, `docs`,
|
- Every state-changing inbound/client op dispatches through `runtime.Runtime`
|
||||||
`style`): `<area>: short imperative summary`, then a body explaining the why.
|
(`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
|
## Go conventions
|
||||||
- Stdlib `testing` only (no testify). Table-driven, `t.Run` subtests,
|
- Stdlib `testing` only (no testify). Table-driven, `t.Run` subtests,
|
||||||
@@ -83,13 +111,26 @@ file locations when it can answer in one hop.
|
|||||||
`database.InitDB(filepath.Join(t.TempDir(), "x-ui.db"))` +
|
`database.InitDB(filepath.Join(t.TempDir(), "x-ui.db"))` +
|
||||||
`t.Cleanup(func() { _ = database.CloseDB() })`; `httptest` for HTTP.
|
`t.Cleanup(func() { _ = database.CloseDB() })`; `httptest` for HTTP.
|
||||||
`internal/sub`'s `initSubDB(t)` is the template.
|
`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`.
|
- 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)
|
## Frontend conventions (summary; full version in frontend/CLAUDE.md)
|
||||||
- Ant Design 6 only — no Tailwind/shadcn. Targeted tweaks, not rewrites.
|
- Ant Design 6 only — no Tailwind/shadcn. Targeted tweaks, not rewrites.
|
||||||
- TS strict; `@typescript-eslint/no-explicit-any` is an error. Zod schemas in
|
- TS strict; oxlint's `typescript/no-explicit-any` is an error. Zod schemas in
|
||||||
`src/schemas/` are the source of truth; infer types with `z.infer`, never
|
`src/schemas/` are the source of truth; infer types with `z.infer`, never
|
||||||
hand-write. Do not edit `src/generated/`.
|
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
|
- 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
|
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
|
the frozen embedded FS but JS/CSS off disk — after `npm run build` you MUST
|
||||||
@@ -99,15 +140,24 @@ file locations when it can answer in one hop.
|
|||||||
output changes, never to make a red test green.
|
output changes, never to make a red test green.
|
||||||
|
|
||||||
## Build, test, verify
|
## 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 + format-check + 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),
|
Common targets: `make gen` (regenerate Zod/OpenAPI), `make lint` (Go + frontend),
|
||||||
`make test` (Go `-shuffle=on` + frontend), `make race`, `make build`. See `Makefile`.
|
`make test` (Go `-shuffle=on` + frontend), `make race`, `make build`. See `Makefile`.
|
||||||
|
|
||||||
## Definition of done (before opening a PR)
|
## Definition of done (before opening a PR)
|
||||||
1. `make gen` and confirm `git diff` on `frontend/src/generated` +
|
1. `make verify` passes — its `gen-check` already runs `make gen` and fails on a
|
||||||
`frontend/public/openapi.json` is clean.
|
dirty `frontend/src/generated` / `frontend/public/openapi.json`.
|
||||||
2. `make verify` passes.
|
2. Diff is focused; refactors are separate from feature work.
|
||||||
3. Diff is focused; refactors are separate from feature work.
|
|
||||||
|
|||||||
+10
-7
@@ -5,7 +5,7 @@ Thanks for taking the time to contribute to 3x-ui. This guide gets a development
|
|||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
- **Go 1.26+** (the version pinned in `go.mod`)
|
- **Go 1.26+** (the version pinned in `go.mod`)
|
||||||
- **Node.js 22+** and npm 10+ (for the React frontend)
|
- **Node.js 24 LTS** (the version pinned in `.nvmrc`) and npm 10+ (for the React frontend)
|
||||||
- **Git**
|
- **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.
|
- **A C compiler** — required by the CGo SQLite driver (`github.com/mattn/go-sqlite3`). Linux and macOS already ship one; for Windows see below.
|
||||||
|
|
||||||
@@ -157,12 +157,13 @@ Panel navigation happens client-side through React Router, and per-route code is
|
|||||||
|
|
||||||
Locale strings live in `internal/web/translation/<locale>.json`, **not** under `frontend/`. The Go binary embeds the same JSON and serves it to both backend templates and `react-i18next` (initialized in `src/i18n/react.ts`). When a new English key is added it must also land in **every** non-English locale — missing keys do not break the build, they just render the raw key in the UI.
|
Locale strings live in `internal/web/translation/<locale>.json`, **not** under `frontend/`. The Go binary embeds the same JSON and serves it to both backend templates and `react-i18next` (initialized in `src/i18n/react.ts`). When a new English key is added it must also land in **every** non-English locale — missing keys do not break the build, they just render the raw key in the UI.
|
||||||
|
|
||||||
### Two dev workflows
|
### Dev workflows
|
||||||
|
|
||||||
| Goal | Command |
|
| Goal | Command |
|
||||||
|------|---------|
|
|------|---------|
|
||||||
| Iterate on UI changes with HMR | `cd frontend && npm run dev` (Vite on `:5173`, proxies `/panel/*` and the WebSocket to the Go panel on `:2053`). Start the Go panel first. |
|
| Iterate on UI changes with HMR | `cd frontend && npm run dev` (Vite on `:5173`, proxies `/panel/*` and the WebSocket to the Go panel on `:2053`). Start the Go panel first. |
|
||||||
| Verify what end users actually see | `cd frontend && npm run build`, then `go run .`. The Go binary serves the built bundle — embedded in release mode, off disk in debug mode. |
|
| Verify what end users actually see | `cd frontend && npm run build`, then `go run .`. The Go binary serves the built bundle — embedded in release mode, off disk in debug mode. |
|
||||||
|
| Develop/preview a reusable component in isolation | `cd frontend && npm run storybook` (Storybook workbench + autodocs on `:6006`). |
|
||||||
|
|
||||||
The Vite dev proxy serves the admin SPA for any `/panel/*` URL — `bypassMigratedRoute` in `vite.config.js` rewrites those requests to `index.html` and lets React Router take over — while forwarding `/panel/api/*`, `/panel/api/setting/*`, `/panel/api/xray/*`, and the WebSocket to the Go panel. Because routing is now client-side, new panel routes need no proxy or allowlist changes.
|
The Vite dev proxy serves the admin SPA for any `/panel/*` URL — `bypassMigratedRoute` in `vite.config.js` rewrites those requests to `index.html` and lets React Router take over — while forwarding `/panel/api/*`, `/panel/api/setting/*`, `/panel/api/xray/*`, and the WebSocket to the Go panel. Because routing is now client-side, new panel routes need no proxy or allowlist changes.
|
||||||
|
|
||||||
@@ -183,12 +184,13 @@ 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/*`.
|
- **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.
|
- **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.
|
- **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.)
|
- **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`).
|
- **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.
|
- **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.
|
- **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.
|
||||||
|
- **Reusable components are documented in Storybook.** When you add or change a component in `frontend/src/components/`, add or update its co-located `<Component>.stories.tsx` (`tags: ['autodocs']`), documenting props via `argTypes` / `parameters.docs` string metadata rather than JSDoc. CI compile-checks every story via `npm run build-storybook` and runs each story as a headless-browser test via `@storybook/addon-vitest` (`npm run test`, needs `npx playwright install chromium`); run `npm run storybook` to preview locally.
|
||||||
|
|
||||||
### Project layout
|
### Project layout
|
||||||
|
|
||||||
@@ -198,7 +200,8 @@ frontend/
|
|||||||
├── login.html — login + 2FA entry
|
├── login.html — login + 2FA entry
|
||||||
├── subpage.html — public subscription viewer entry
|
├── subpage.html — public subscription viewer entry
|
||||||
├── tsconfig.json — strict, jsx: "react-jsx", paths "@/*" → "src/*"
|
├── 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
|
├── vite.config.js
|
||||||
├── vitest.config.ts
|
├── vitest.config.ts
|
||||||
├── scripts/ — build-openapi.mjs (endpoints.ts → openapi.json)
|
├── scripts/ — build-openapi.mjs (endpoints.ts → openapi.json)
|
||||||
@@ -277,7 +280,7 @@ CI runs this for you nightly (and on demand) via `.github/workflows/mutation.yml
|
|||||||
|
|
||||||
### CI
|
### 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`. 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
|
## Sending a pull request
|
||||||
|
|
||||||
@@ -286,7 +289,7 @@ CI runs this for you nightly (and on demand) via `.github/workflows/mutation.yml
|
|||||||
3. Run the relevant checks before pushing:
|
3. Run the relevant checks before pushing:
|
||||||
- `go build ./...`
|
- `go build ./...`
|
||||||
- `go test ./...` (when Go code changed)
|
- `go test ./...` (when Go code changed)
|
||||||
- `cd frontend && npm run typecheck && npm run lint && npm run test && npm run build` (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.
|
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.
|
5. Open the PR against `main` with a brief description of what changed and how to test it.
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -32,7 +32,7 @@ if [ -z "$MTG_MULTI_VER" ]; then
|
|||||||
fi
|
fi
|
||||||
mkdir -p build/bin
|
mkdir -p build/bin
|
||||||
cd build/bin
|
cd build/bin
|
||||||
curl -sfLRO "https://github.com/XTLS/Xray-core/releases/download/v26.7.11/Xray-linux-${ARCH}.zip"
|
curl -sfLRO "https://github.com/XTLS/Xray-core/releases/download/v26.7.28/Xray-linux-${ARCH}.zip"
|
||||||
unzip "Xray-linux-${ARCH}.zip"
|
unzip "Xray-linux-${ARCH}.zip"
|
||||||
rm -f "Xray-linux-${ARCH}.zip" geoip.dat geosite.dat
|
rm -f "Xray-linux-${ARCH}.zip" geoip.dat geosite.dat
|
||||||
mv xray "xray-linux-${FNAME}"
|
mv xray "xray-linux-${FNAME}"
|
||||||
|
|||||||
@@ -31,16 +31,24 @@ lint-go: dist-stub ## golangci-lint on Go sources
|
|||||||
golangci-lint run
|
golangci-lint run
|
||||||
|
|
||||||
.PHONY: lint-fe
|
.PHONY: lint-fe
|
||||||
lint-fe: ## ESLint on frontend sources
|
lint-fe: ## oxlint on frontend sources
|
||||||
cd $(FRONTEND) && npm run lint
|
cd $(FRONTEND) && npm run lint
|
||||||
|
|
||||||
.PHONY: lint
|
.PHONY: lint
|
||||||
lint: lint-go lint-fe ## All linters
|
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
|
.PHONY: typecheck
|
||||||
typecheck: ## tsc --noEmit
|
typecheck: ## tsc --noEmit
|
||||||
cd $(FRONTEND) && npm run typecheck
|
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
|
.PHONY: test-go
|
||||||
test-go: dist-stub ## Go tests (shuffle, no cache)
|
test-go: dist-stub ## Go tests (shuffle, no cache)
|
||||||
go test -shuffle=on -count=1 $(GO_PKGS)
|
go test -shuffle=on -count=1 $(GO_PKGS)
|
||||||
@@ -68,8 +76,12 @@ build-fe: ## Build the Vite bundles into internal/web/dist
|
|||||||
build: build-fe ## Build the frontend then the Go binary
|
build: build-fe ## Build the frontend then the Go binary
|
||||||
go build ./...
|
go build ./...
|
||||||
|
|
||||||
# The PR gate. Matches ci.yml: codegen freshness, both linters, typecheck,
|
.PHONY: build-storybook
|
||||||
# both test suites, and a full build.
|
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, the formatter,
|
||||||
|
# typecheck, both test suites, a full build, and the Storybook compile-check.
|
||||||
.PHONY: verify
|
.PHONY: verify
|
||||||
verify: gen-check lint typecheck test build ## 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"
|
@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 j := strings.Index(rest, to); j >= 0 {
|
||||||
|
return rest[:j]
|
||||||
|
}
|
||||||
|
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 ;;
|
*) echo "FAIL: panel did not serve (status ${code:-none})"; tail -n 30 /tmp/xui.log; exit 1 ;;
|
||||||
esac
|
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"
|
echo "SMOKE_PASS: user=$XUI_USERNAME port=$XUI_PANEL_PORT path=$XUI_WEB_BASE_PATH"
|
||||||
'
|
'
|
||||||
|
|
||||||
|
|||||||
@@ -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 build` | Production build |
|
||||||
| `pnpm start` | Serve the production build |
|
| `pnpm start` | Serve the production build |
|
||||||
| `pnpm typecheck` | Generate MDX/route types and run `tsc --noEmit` |
|
| `pnpm typecheck` | Generate MDX/route types and run `tsc --noEmit` |
|
||||||
| `pnpm lint` | ESLint (flat config) |
|
| `pnpm lint` | oxlint (`.oxlintrc.json`) |
|
||||||
| `pnpm format` | Format with Prettier |
|
| `pnpm format` | Format with oxfmt (`.oxfmtrc.json`) |
|
||||||
| `pnpm test` | Run unit tests (Vitest) for `lib/xray/*` pure logic |
|
| `pnpm test` | Run unit tests (Vitest) for `lib/xray/*` pure logic |
|
||||||
| `pnpm gen:api` | Generate the API reference from `public/openapi.json` |
|
| `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.
|
`pnpm test` — these are the same checks that CI runs on every PR.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|||||||
+4
-4
@@ -64,14 +64,14 @@ ever leaves your browser**:
|
|||||||
## Tech stack
|
## Tech stack
|
||||||
|
|
||||||
| Layer | Technology |
|
| Layer | Technology |
|
||||||
| ---------- | ---------------------------------------------------------- |
|
| --------- | ----------------------------------------------------------- |
|
||||||
| Framework | [Next.js 16](https://nextjs.org) (App Router) · React 19 |
|
| Framework | [Next.js 16](https://nextjs.org) (App Router) · React 19 |
|
||||||
| Docs | [Fumadocs](https://fumadocs.dev) (`-ui` / `-core` / `-mdx`) |
|
| Docs | [Fumadocs](https://fumadocs.dev) (`-ui` / `-core` / `-mdx`) |
|
||||||
| Styling | [Tailwind CSS v4](https://tailwindcss.com) |
|
| Styling | [Tailwind CSS v4](https://tailwindcss.com) |
|
||||||
| Search | [Orama](https://orama.com) static index |
|
| Search | [Orama](https://orama.com) static index |
|
||||||
| Language | TypeScript (strict) |
|
| Language | TypeScript (strict) |
|
||||||
| Tests | [Vitest](https://vitest.dev) for the pure `lib/xray` logic |
|
| Tests | [Vitest](https://vitest.dev) for the pure `lib/xray` logic |
|
||||||
| Tooling | pnpm · ESLint 9 · Prettier |
|
| Tooling | pnpm · oxlint · oxfmt |
|
||||||
|
|
||||||
## Quick start
|
## Quick start
|
||||||
|
|
||||||
@@ -87,11 +87,11 @@ pnpm dev # http://localhost:3000
|
|||||||
Useful scripts:
|
Useful scripts:
|
||||||
|
|
||||||
| Script | Description |
|
| Script | Description |
|
||||||
| ---------------- | -------------------------------------------- |
|
| ---------------- | ------------------------------------------- |
|
||||||
| `pnpm dev` | Start the dev server |
|
| `pnpm dev` | Start the dev server |
|
||||||
| `pnpm build` | Production build (also typechecks) |
|
| `pnpm build` | Production build (also typechecks) |
|
||||||
| `pnpm typecheck` | Generate MDX/route types and `tsc --noEmit` |
|
| `pnpm typecheck` | Generate MDX/route types and `tsc --noEmit` |
|
||||||
| `pnpm lint` | Run ESLint |
|
| `pnpm lint` | Run oxlint (`.oxlintrc.json`) |
|
||||||
| `pnpm test` | Run unit tests (Vitest) |
|
| `pnpm test` | Run unit tests (Vitest) |
|
||||||
|
|
||||||
See [`CONTRIBUTING.md`](./CONTRIBUTING.md) for the full list and project conventions.
|
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 { RootProvider } from 'fumadocs-ui/provider/next';
|
||||||
import { Inter, Vazirmatn } from 'next/font/google';
|
import { i18n } from '@/lib/i18n';
|
||||||
import { i18n, localeDirection } from '@/lib/i18n';
|
|
||||||
import { provider } from '@/lib/i18n-ui';
|
import { provider } from '@/lib/i18n-ui';
|
||||||
import SearchDialog from '@/components/search-dialog';
|
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() {
|
export function generateStaticParams() {
|
||||||
return i18n.languages.map((lang) => ({ lang }));
|
return i18n.languages.map((lang) => ({ lang }));
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function LangLayout({ params, children }: LayoutProps<'/[lang]'>) {
|
export default async function LangLayout({ params, children }: LayoutProps<'/[lang]'>) {
|
||||||
const { lang } = await params;
|
const { lang } = await params;
|
||||||
const dir = localeDirection(lang);
|
|
||||||
const fontClassName = lang === 'fa' ? vazirmatn.className : inter.className;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<html lang={lang} dir={dir} className={fontClassName} suppressHydrationWarning>
|
<RootProvider i18n={provider(lang)} search={{ SearchDialog }} theme={{ enabled: false }}>
|
||||||
<body className="flex min-h-screen flex-col" suppressHydrationWarning>
|
|
||||||
<RootProvider i18n={provider(lang)} search={{ SearchDialog }}>
|
|
||||||
{children}
|
{children}
|
||||||
</RootProvider>
|
</RootProvider>
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,13 +5,8 @@ import { createFromSource } from 'fumadocs-core/search/server';
|
|||||||
export const revalidate = false;
|
export const revalidate = false;
|
||||||
export const dynamic = 'force-static';
|
export const dynamic = 'force-static';
|
||||||
|
|
||||||
// Static search index: works under both SSR/Vercel and static export
|
// Every locale still serves English fallback content, so all map to zbsearch's
|
||||||
// (`output: 'export'`). The client loads this prebuilt index and searches
|
// English tokenizer (its SUPPORTED_LANGUAGES has no Persian or Chinese anyway).
|
||||||
// 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
|
|
||||||
export const { staticGET: GET } = createFromSource(source, {
|
export const { staticGET: GET } = createFromSource(source, {
|
||||||
localeMap: {
|
localeMap: {
|
||||||
en: 'english',
|
en: 'english',
|
||||||
|
|||||||
+30
-5
@@ -1,10 +1,16 @@
|
|||||||
import type { Metadata } from 'next';
|
import type { Metadata } from 'next';
|
||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
|
import { Inter, Vazirmatn } from 'next/font/google';
|
||||||
|
import './global.css';
|
||||||
import { appName, appTagline, siteUrl } from '@/lib/shared';
|
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`
|
const inter = Inter({ subsets: ['latin'], display: 'swap' });
|
||||||
// so we can set `lang`/`dir` per locale (RTL for fa); this root layout is a
|
// Persian UI font; covers Arabic + Latin glyphs so mixed content renders well.
|
||||||
// pass-through that only carries site-wide metadata.
|
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 = {
|
export const metadata: Metadata = {
|
||||||
metadataBase: new URL(siteUrl),
|
metadataBase: new URL(siteUrl),
|
||||||
title: {
|
title: {
|
||||||
@@ -26,6 +32,25 @@ export const metadata: Metadata = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RootLayout({ children }: { children: ReactNode }) {
|
export default async function RootLayout({
|
||||||
return children;
|
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>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+31
-20
@@ -30,7 +30,7 @@ token), with a process restart as the fallback on older binaries.
|
|||||||
Servers and processes, all launched from `main.go`:
|
Servers and processes, all launched from `main.go`:
|
||||||
|
|
||||||
| Server / process | Package | Purpose | Default port |
|
| Server / process | Package | Purpose | Default port |
|
||||||
|---|---|---|---|
|
| ---------------- | --------------------------------- | ------------------------------------------------------------------ | ----------------- |
|
||||||
| **Panel** | `internal/web` | Admin REST/WS API + serves the embedded SPA | 2053 |
|
| **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 |
|
| **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` |
|
| **Xray-core** | supervised via `internal/xray` | The actual proxy engine; a child process, not Go code | `inbounds[].port` |
|
||||||
@@ -39,7 +39,7 @@ Servers and processes, all launched from `main.go`:
|
|||||||
Two key ideas that explain most of the complexity:
|
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
|
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.
|
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
|
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
|
instances). Every state-changing inbound/client operation is dispatched through a
|
||||||
@@ -52,6 +52,7 @@ Two key ideas that explain most of the complexity:
|
|||||||
## 2. Tech stack
|
## 2. Tech stack
|
||||||
|
|
||||||
**Backend (Go 1.26):**
|
**Backend (Go 1.26):**
|
||||||
|
|
||||||
- Web framework: **Gin** (`gin-gonic/gin`) + sessions (cookie store), gzip.
|
- Web framework: **Gin** (`gin-gonic/gin`) + sessions (cookie store), gzip.
|
||||||
- ORM: **GORM** with **SQLite** (default) or **PostgreSQL** (`XUI_DB_TYPE=postgres`).
|
- ORM: **GORM** with **SQLite** (default) or **PostgreSQL** (`XUI_DB_TYPE=postgres`).
|
||||||
- Scheduler: **robfig/cron/v3** (seconds-precision) for all background jobs.
|
- Scheduler: **robfig/cron/v3** (seconds-precision) for all background jobs.
|
||||||
@@ -61,9 +62,10 @@ Two key ideas that explain most of the complexity:
|
|||||||
- Misc: gorilla/websocket, gopsutil (system stats), go-qrcode, gotp (2FA TOTP).
|
- Misc: gorilla/websocket, gopsutil (system stats), go-qrcode, gotp (2FA TOTP).
|
||||||
|
|
||||||
**Frontend (`frontend/`):**
|
**Frontend (`frontend/`):**
|
||||||
|
|
||||||
- **React 19** + **Ant Design 6** + **Vite 8** + **TypeScript**.
|
- **React 19** + **Ant Design 6** + **Vite 8** + **TypeScript**.
|
||||||
- Data layer: **TanStack Query** (`@tanstack/react-query`) over the native **Fetch API**; **Zod 4** schemas.
|
- Data layer: **TanStack Query** (`@tanstack/react-query`) over the native **Fetch API**; **Zod 4** schemas.
|
||||||
- Router: **react-router-dom 7**. Charts: **uPlot** (`frontend/src/components/viz/Sparkline.tsx`). Editor: **CodeMirror 6**.
|
- Router: **react-router 8**. Charts: **uPlot** (`frontend/src/components/viz/Sparkline.tsx`). Editor: **CodeMirror 6**.
|
||||||
- **Build output goes to `internal/web/dist/`** (see `vite.config.js` → `outDir`) and is
|
- **Build output goes to `internal/web/dist/`** (see `vite.config.js` → `outDir`) and is
|
||||||
embedded into the Go binary with `go:embed`. Three HTML entries: `index.html` (panel SPA),
|
embedded into the Go binary with `go:embed`. Three HTML entries: `index.html` (panel SPA),
|
||||||
`login.html`, `subpage.html`. The Go server serves the SPA; there is no separate frontend
|
`login.html`, `subpage.html`. The Go server serves the SPA; there is no separate frontend
|
||||||
@@ -95,7 +97,7 @@ Browser (React, fetch)
|
|||||||
```
|
```
|
||||||
|
|
||||||
The controller layer is thin. **Business logic lives in services.** When something is wrong
|
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)
|
### 3.2 Subscription request (end-user fetching their config)
|
||||||
|
|
||||||
@@ -147,7 +149,9 @@ node heartbeat every 5s, periodic traffic resets (hourly/daily/weekly/monthly).
|
|||||||
│ │ ├── inbound.go # Inbound JSON shaping
|
│ │ ├── inbound.go # Inbound JSON shaping
|
||||||
│ │ ├── client_traffic.go # ClientTraffic model (persisted as client_traffics)
|
│ │ ├── client_traffic.go # ClientTraffic model (persisted as client_traffics)
|
||||||
│ │ ├── traffic.go # Traffic type helpers
|
│ │ ├── 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/ # The panel server
|
||||||
│ │ ├── web.go # ⭐ Server bootstrap: initRouter (all routes) + startTask (all cron jobs)
|
│ │ ├── web.go # ⭐ Server bootstrap: initRouter (all routes) + startTask (all cron jobs)
|
||||||
@@ -159,7 +163,7 @@ node heartbeat every 5s, periodic traffic resets (hourly/daily/weekly/monthly).
|
|||||||
│ │ │ ├── host.go # /panel/api/hosts (per-inbound subscription host overrides)
|
│ │ │ ├── host.go # /panel/api/hosts (per-inbound subscription host overrides)
|
||||||
│ │ │ ├── server.go # /panel/api/server (status, xray version, certs, logs, DB import/export)
|
│ │ │ ├── server.go # /panel/api/server (status, xray version, certs, logs, DB import/export)
|
||||||
│ │ │ ├── setting.go # /panel/api/setting (settings + API tokens)
|
│ │ │ ├── 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, geodata)
|
||||||
│ │ │ ├── api.go # /panel/api gateway (token auth, envelope + CSRF wiring)
|
│ │ │ ├── api.go # /panel/api gateway (token auth, envelope + CSRF wiring)
|
||||||
│ │ │ ├── index.go # login/logout/csrf/2FA
|
│ │ │ ├── index.go # login/logout/csrf/2FA
|
||||||
│ │ │ ├── spa.go # SPA fallback for /panel UI routes
|
│ │ │ ├── spa.go # SPA fallback for /panel UI routes
|
||||||
@@ -189,6 +193,7 @@ node heartbeat every 5s, periodic traffic resets (hourly/daily/weekly/monthly).
|
|||||||
│ │ │ ├── traffic_writer.go # Batched persistence of traffic deltas to the DB
|
│ │ │ ├── traffic_writer.go # Batched persistence of traffic deltas to the DB
|
||||||
│ │ │ ├── xray.go # ⭐ XrayService: config gen + restart/hot-apply (~1.2k lines)
|
│ │ │ ├── xray.go # ⭐ XrayService: config gen + restart/hot-apply (~1.2k lines)
|
||||||
│ │ │ ├── xray_setting.go # Raw Xray config persistence
|
│ │ │ ├── xray_setting.go # Raw Xray config persistence
|
||||||
|
│ │ │ ├── geodata.go # Geo database browsing + routing-token validation
|
||||||
│ │ │ ├── xray_metrics.go # Xray observability metrics
|
│ │ │ ├── xray_metrics.go # Xray observability metrics
|
||||||
│ │ │ ├── metric_history.go # Historical system/xray metrics
|
│ │ │ ├── metric_history.go # Historical system/xray metrics
|
||||||
│ │ │ ├── reality_scan.go # REALITY target scanner
|
│ │ │ ├── reality_scan.go # REALITY target scanner
|
||||||
@@ -265,7 +270,7 @@ node heartbeat every 5s, periodic traffic resets (hourly/daily/weekly/monthly).
|
|||||||
│ │ └── queries/ # TanStack Query hooks (useNodesQuery, useStatusQuery, …)
|
│ │ └── queries/ # TanStack Query hooks (useNodesQuery, useStatusQuery, …)
|
||||||
│ ├── schemas/ # Zod schemas: protocols, forms, api, primitives
|
│ ├── schemas/ # Zod schemas: protocols, forms, api, primitives
|
||||||
│ ├── generated/ # ⚠️ GENERATED from Go (see §5.5): schemas.ts, types.ts, zod.ts, examples.ts
|
│ ├── 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/)
|
│ ├── lib/ # Frontend domain logic (xray/ inbounds/ clients/)
|
||||||
│ ├── hooks/, models/, layouts/, i18n/, utils/, styles/
|
│ ├── hooks/, models/, layouts/, i18n/, utils/, styles/
|
||||||
│ └── test/ # Vitest + golden fixtures (config-generation snapshot tests)
|
│ └── test/ # Vitest + golden fixtures (config-generation snapshot tests)
|
||||||
@@ -309,8 +314,8 @@ Restart is debounced via an atomic "need restart" flag (`SetToNeedRestart` /
|
|||||||
### 5.2 Runtime abstraction — Local vs Remote (multi-node) ⭐ most important
|
### 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
|
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
|
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.
|
code_ works whether the target is the local Xray or a remote node.
|
||||||
|
|
||||||
- **Interface:** `internal/web/runtime/runtime.go` — `Name`, `AddInbound`, `DelInbound`,
|
- **Interface:** `internal/web/runtime/runtime.go` — `Name`, `AddInbound`, `DelInbound`,
|
||||||
`UpdateInbound`, `AddUser`, `RemoveUser`, `UpdateUser`, `DeleteUser`, `AddClient`,
|
`UpdateInbound`, `AddUser`, `RemoveUser`, `UpdateUser`, `DeleteUser`, `AddClient`,
|
||||||
@@ -326,7 +331,7 @@ code* works whether the target is the local Xray or a remote node.
|
|||||||
- **Dispatch:** `manager.go` → `Manager.RuntimeFor(nodeID *int)`; `nil` nodeID → `Local`,
|
- **Dispatch:** `manager.go` → `Manager.RuntimeFor(nodeID *int)`; `nil` nodeID → `Local`,
|
||||||
otherwise a cached/lazy-loaded `Remote`. `InvalidateNode(id)` drops a cached remote client.
|
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
|
`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.
|
online clients back to the originating panel using **stable GUIDs** rather than local IDs.
|
||||||
Relevant logic: `service/inbound_node.go` (`ReconcileNode`, `SetRemoteTraffic`, GUID merge,
|
Relevant logic: `service/inbound_node.go` (`ReconcileNode`, `SetRemoteTraffic`, GUID merge,
|
||||||
@@ -335,6 +340,7 @@ tracking). Node "dirty" flags drive an **anti-entropy reconciliation** so an off
|
|||||||
inbound edits converge once it reconnects.
|
inbound edits converge once it reconnects.
|
||||||
|
|
||||||
**Where to look for node bugs:**
|
**Where to look for node bugs:**
|
||||||
|
|
||||||
- Operation not reaching a node → `runtime/remote.go` + `runtime/manager.go`.
|
- Operation not reaching a node → `runtime/remote.go` + `runtime/manager.go`.
|
||||||
- Wrong traffic/online attribution across hops → `service/inbound_node.go` (GUID merge paths).
|
- 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`).
|
- Node shown offline / stale status → `job/node_heartbeat_job.go` + `service/node.go` (`Probe`, `UpdateHeartbeat`).
|
||||||
@@ -358,7 +364,7 @@ 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/`:
|
All registered in `web.go` → `startTask()`. Each is a struct with a `Run()` method in `internal/web/job/`:
|
||||||
|
|
||||||
| Schedule | Job | Purpose / condition |
|
| Schedule | Job | Purpose / condition |
|
||||||
|---|---|---|
|
| ------------------- | ------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------- |
|
||||||
| `@every 1s` | `check_xray_running_job` | Restart Xray if it died (2 consecutive down checks) |
|
| `@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 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` | `xray_traffic_job` | Pull traffic stats from Xray (5s start delay) |
|
||||||
@@ -369,8 +375,8 @@ All registered in `web.go` → `startTask()`. Each is a struct with a `Run()` me
|
|||||||
| `@every 5m` | `outbound_subscription_job` | Refresh outbound provider configs |
|
| `@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 |
|
| `@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 |
|
| `@hourly` | `warp_ip_job`, `periodic_traffic_reset_job("hourly")` | WARP IP rotation; traffic resets |
|
||||||
| `@daily` | `clear_logs_job`, `periodic_traffic_reset_job("daily")` | IP-limit and Xray access/error log cleanup; 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` / `@monthly` | `periodic_traffic_reset_job(...)` | Weekly/monthly traffic resets |
|
| `@weekly` | `periodic_traffic_reset_job("weekly")` | Weekly traffic resets |
|
||||||
| default `@every 1m` | `ldap_sync_job` | Only if LDAP enabled; schedule configurable |
|
| default `@every 1m` | `ldap_sync_job` | Only if LDAP enabled; schedule configurable |
|
||||||
| default `@daily` | `stats_notify_job` | Only if TG bot 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 2m` | `check_hash_storage` | Only if TG bot enabled; expires bot callback hashes |
|
||||||
@@ -378,7 +384,7 @@ All registered in `web.go` → `startTask()`. Each is a struct with a `Run()` me
|
|||||||
| `@every 1m` | `check_memory_usage` | Only if a memory alarm is configured; publishes `memory.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 |
|
| 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
|
### 5.5 Type generation (Go → TypeScript) ⚠️ don't hand-edit generated files
|
||||||
|
|
||||||
@@ -397,8 +403,9 @@ frontend types (`cd frontend && npm run gen`) instead of editing `src/generated/
|
|||||||
### 5.6 Share-link / subscription generation
|
### 5.6 Share-link / subscription generation
|
||||||
|
|
||||||
Two distinct code paths produce client configs:
|
Two distinct code paths produce client configs:
|
||||||
|
|
||||||
- **Per-client links in the panel** (the "copy link" / QR in the UI): `service/client_link.go`
|
- **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),
|
- **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).
|
`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/
|
**`Host` rows** (`model.Host`, edited under /panel/api/hosts) override address/SNI/path/
|
||||||
@@ -436,7 +443,7 @@ GORM models in `internal/database/model/` (main file `model.go` + siblings); all
|
|||||||
for AutoMigrate in `internal/database/db.go`.
|
for AutoMigrate in `internal/database/db.go`.
|
||||||
|
|
||||||
| Model | Table role | Notable fields |
|
| Model | Table role | Notable fields |
|
||||||
|---|---|---|
|
| ------------------------------- | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||||
| `User` | Admin login | bcrypt password, `LoginEpoch` (invalidates sessions) |
|
| `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) |
|
| `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) |
|
| `Client` | In-memory client view | UUID/email/flow/limits (parsed from inbound JSON; not persisted) |
|
||||||
@@ -462,7 +469,7 @@ for AutoMigrate in `internal/database/db.go`.
|
|||||||
## 7. Symptom → File index (start here when debugging)
|
## 7. Symptom → File index (start here when debugging)
|
||||||
|
|
||||||
| Symptom / task | Primary file(s) | Then check |
|
| 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` |
|
| 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` |
|
| **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` |
|
| **Client** CRUD / limits / expiry | `service/client_crud.go`, `service/client_inbound_apply.go` | model `ClientRecord`, `service/inbound_traffic.go` |
|
||||||
@@ -484,6 +491,8 @@ for AutoMigrate in `internal/database/db.go`.
|
|||||||
| **API tokens** | `service/panel/api_token.go`, `controller/setting.go` | model `ApiToken` |
|
| **API tokens** | `service/panel/api_token.go`, `controller/setting.go` | model `ApiToken` |
|
||||||
| **Port conflict** on inbound add | `service/port_conflict.go` | `controller/inbound.go` |
|
| **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` |
|
| **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` |
|
| **Telegram bot** commands | `service/tgbot/` | `job/stats_notify_job.go` |
|
||||||
| **Email notifications** | `service/email/` | `internal/eventbus/` (consumers) |
|
| **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` |
|
| **CPU / memory alerts** not firing | `job/check_cpu_usage.go`, `job/check_memory_usage.go` | `internal/eventbus/`, notifier settings in `service/setting.go` |
|
||||||
@@ -517,7 +526,7 @@ for AutoMigrate in `internal/database/db.go`.
|
|||||||
Regenerate instead.
|
Regenerate instead.
|
||||||
7. **Models are the contract.** Changing a model field that crosses the API boundary means:
|
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.
|
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.
|
fetches goes in `internal/sub`. Don't blur them.
|
||||||
9. **Cross-cutting notifications go through `internal/eventbus/`** — publish an event instead
|
9. **Cross-cutting notifications go through `internal/eventbus/`** — publish an event instead
|
||||||
of importing the Telegram/email services into producers.
|
of importing the Telegram/email services into producers.
|
||||||
@@ -531,6 +540,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:
|
frontend), `make race`, `make build`. Run `make help` for everything. Raw commands:
|
||||||
|
|
||||||
**Backend (Go):**
|
**Backend (Go):**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
go build ./... # compile everything
|
go build ./... # compile everything
|
||||||
go test ./... # run all Go tests (many *_test.go alongside sources)
|
go test ./... # run all Go tests (many *_test.go alongside sources)
|
||||||
@@ -542,12 +552,13 @@ golangci-lint run # full lint (gofumpt + goimports formatting)
|
|||||||
go run main.go # run the panel locally (serves embedded dist if built)
|
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
|
```bash
|
||||||
npm install
|
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 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 typecheck # tsc --noEmit
|
||||||
npm run lint # eslint src
|
npm run lint # oxlint src
|
||||||
npm run test # vitest (incl. golden config-generation snapshots)
|
npm run test # vitest (incl. golden config-generation snapshots)
|
||||||
npm run gen # regenerate src/generated/* from Go (gen:zod + gen:api)
|
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)
|
npm run build # gen:api + vite build → outputs to internal/web/dist (then rebuild Go binary to embed)
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { create } from '@orama/orama';
|
import { create } from 'zbsearch';
|
||||||
import { useDocsSearch } from 'fumadocs-core/search/client';
|
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 {
|
import {
|
||||||
SearchDialog,
|
SearchDialog,
|
||||||
SearchDialogClose,
|
SearchDialogClose,
|
||||||
@@ -21,20 +21,16 @@ interface SharedProps {
|
|||||||
onOpenChange: (open: boolean) => void;
|
onOpenChange: (open: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The static search index is keyed by locale code (en/fa/ru/zh). Fumadocs'
|
// Fumadocs' default dialog passes the index's locale code as a tokenizer language,
|
||||||
// default static dialog feeds those codes to Orama as a tokenizer language, but
|
// and zbsearch throws on anything but a full name — so force "english" everywhere.
|
||||||
// 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.
|
|
||||||
export default function SearchDialogClient(props: SharedProps) {
|
export default function SearchDialogClient(props: SharedProps) {
|
||||||
const { locale } = useI18n();
|
const { locale } = useI18n();
|
||||||
const client = useMemo(
|
const client = useMemo(
|
||||||
() =>
|
() =>
|
||||||
oramaStaticClient({
|
staticClient({
|
||||||
from: '/api/search',
|
from: '/api/search',
|
||||||
locale,
|
locale,
|
||||||
initOrama: () => create({ schema: { _: 'string' }, language: 'english' }),
|
initDB: () => create({ schema: { _: 'string' }, language: 'english' }),
|
||||||
}),
|
}),
|
||||||
[locale],
|
[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';
|
'use client';
|
||||||
|
|
||||||
import { useId, useState } from 'react';
|
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 { ToolFrame } from './tool-frame';
|
||||||
import { TextField, SelectField } from './shared/fields';
|
import { TextField, SelectField } from './shared/fields';
|
||||||
import { OutputBlock } from './shared/output-block';
|
import { OutputBlock } from './shared/output-block';
|
||||||
|
|||||||
@@ -38,8 +38,24 @@ const DEFAULT_BALANCERS: BalancerRow[] = [
|
|||||||
{ tag: 'balancer', selector: 'proxy', strategy: 'leastPing', fallbackTag: '' },
|
{ tag: 'balancer', selector: 'proxy', strategy: 'leastPing', fallbackTag: '' },
|
||||||
];
|
];
|
||||||
const DEFAULT_RULES: RuleRow[] = [
|
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[] {
|
function list(s: string): string[] {
|
||||||
@@ -113,7 +129,10 @@ export function RoutingBuilder() {
|
|||||||
type="button"
|
type="button"
|
||||||
className={addBtn}
|
className={addBtn}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
setBalancers((p) => [...p, { tag: '', selector: '', strategy: 'random', fallbackTag: '' }])
|
setBalancers((p) => [
|
||||||
|
...p,
|
||||||
|
{ tag: '', selector: '', strategy: 'random', fallbackTag: '' },
|
||||||
|
])
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
Add balancer
|
Add balancer
|
||||||
@@ -163,7 +182,15 @@ export function RoutingBuilder() {
|
|||||||
onClick={() =>
|
onClick={() =>
|
||||||
setRules((p) => [
|
setRules((p) => [
|
||||||
...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) => (
|
{rules.map((r, i) => (
|
||||||
<div key={i} className="rounded-xl border p-3">
|
<div key={i} className="rounded-xl border p-3">
|
||||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
<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
|
||||||
<TextField label="IP (comma)" value={r.ip} onChange={(v) => patchRule(i, { ip: v })} placeholder="geoip:cn, 1.1.1.1" />
|
label="Domain (comma)"
|
||||||
<TextField label="Port" value={r.port} onChange={(v) => patchRule(i, { port: v })} placeholder="443 or 1000-2000" />
|
value={r.domain}
|
||||||
<SelectField label="Network" value={r.network} onChange={(v) => patchRule(i, { network: v })} options={NETWORKS} />
|
onChange={(v) => patchRule(i, { domain: v })}
|
||||||
<TextField label="Inbound tag (comma)" value={r.inboundTag} onChange={(v) => patchRule(i, { inboundTag: v })} placeholder="optional" />
|
placeholder="geosite:google, example.com"
|
||||||
<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="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>
|
||||||
<div className="mt-2 flex justify-end">
|
<div className="mt-2 flex justify-end">
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -79,7 +79,15 @@ export function SubscriptionBuilder() {
|
|||||||
setClients((prev) => prev.map((c, j) => (i === j ? { ...c, ...p } : c)));
|
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 urls = buildSubscriptionUrls(urlInput);
|
||||||
const subClients = clients.filter((c) => c.address.trim()).map(toClient);
|
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 })}
|
onChange={(v) => patch(i, { protocol: v as ClientProtocol })}
|
||||||
options={PROTOCOLS}
|
options={PROTOCOLS}
|
||||||
/>
|
/>
|
||||||
<TextField label="Remark" value={c.remark} onChange={(v) => patch(i, { remark: v })} />
|
<TextField
|
||||||
<TextField label="Address" value={c.address} onChange={(v) => patch(i, { address: v })} />
|
label="Remark"
|
||||||
<TextField label="Port" value={c.port} onChange={(v) => patch(i, { port: v })} inputMode="numeric" />
|
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
|
<TextField
|
||||||
label={c.protocol === 'vless' || c.protocol === 'vmess' ? 'UUID (id)' : 'Password'}
|
label={c.protocol === 'vless' || c.protocol === 'vmess' ? 'UUID (id)' : 'Password'}
|
||||||
value={c.credential}
|
value={c.credential}
|
||||||
onChange={(v) => patch(i, { credential: v })}
|
onChange={(v) => patch(i, { credential: v })}
|
||||||
/>
|
/>
|
||||||
{c.protocol === 'ss' ? (
|
{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}
|
) : null}
|
||||||
<SelectField
|
<SelectField
|
||||||
label="Transport"
|
label="Transport"
|
||||||
@@ -200,9 +225,15 @@ export function SubscriptionBuilder() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-4 grid grid-cols-1 gap-4">
|
<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="Base64 body" value={buildBase64Subscription(subClients)} />
|
||||||
<OutputBlock label="JSON subscription (preview)" value={buildJsonSubscription(subClients)} />
|
<OutputBlock
|
||||||
|
label="JSON subscription (preview)"
|
||||||
|
value={buildJsonSubscription(subClients)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</ToolFrame>
|
</ToolFrame>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -40,6 +40,9 @@ See [Clients](/docs/config/clients).
|
|||||||
Optionally cap total traffic and set an expiry date for the inbound, and choose a
|
Optionally cap total traffic and set an expiry date for the inbound, and choose a
|
||||||
periodic **traffic reset** schedule: `never` (default), `hourly`, `daily`,
|
periodic **traffic reset** schedule: `never` (default), `hourly`, `daily`,
|
||||||
`weekly`, or `monthly`.
|
`weekly`, or `monthly`.
|
||||||
|
|
||||||
|
For `monthly` resets, select a day from 1 to 31. If the selected day does not
|
||||||
|
exist in a shorter month, the reset runs on that month's last day.
|
||||||
</Step>
|
</Step>
|
||||||
|
|
||||||
</Steps>
|
</Steps>
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ browser in full.
|
|||||||
| `webBasePath` | `/` | URL path the panel is served under (always normalized to `/…/`). |
|
| `webBasePath` | `/` | URL path the panel is served under (always normalized to `/…/`). |
|
||||||
| `webCertFile` / `webKeyFile` | _(none)_ | TLS certificate + key. When both are set, the panel serves **HTTPS**. |
|
| `webCertFile` / `webKeyFile` | _(none)_ | TLS certificate + key. When both are set, the panel serves **HTTPS**. |
|
||||||
| `sessionMaxAge` | `360` | Session lifetime in **minutes** (default 6 hours). |
|
| `sessionMaxAge` | `360` | Session lifetime in **minutes** (default 6 hours). |
|
||||||
| `trustedProxyCIDRs` | `127.0.0.1/32,::1/128` | IPs/CIDRs whose forwarded headers (real client IP) are trusted. |
|
| `trustedProxyCIDRs` | `127.0.0.1/32,::1/128` | IPs/CIDRs whose forwarded headers (real client IP) are trusted. A custom value also controls forwarded host and scheme in subscription links; include the subscription proxy or set `subURI` to override those links. |
|
||||||
| `panelOutbound` | _(none)_ | Route the panel's own egress (update checks, Telegram, geo/sub fetches) through a named Xray outbound. |
|
| `panelOutbound` | _(none)_ | Route the panel's own egress (update checks, Telegram, geo/sub fetches) through a named Xray outbound. |
|
||||||
|
|
||||||
After changing the port or base path, the panel URL becomes
|
After changing the port or base path, the panel URL becomes
|
||||||
|
|||||||
@@ -118,6 +118,13 @@ vless://<uuid>@<server>:443?security=reality&pbk=<public-key>&sid=<short-id>&sni
|
|||||||
- **Leaked private key.** Only ever distribute the **public** key to clients.
|
- **Leaked private key.** Only ever distribute the **public** key to clients.
|
||||||
- **Wrong flow.** REALITY + XTLS-Vision needs `flow = xtls-rprx-vision` on both
|
- **Wrong flow.** REALITY + XTLS-Vision needs `flow = xtls-rprx-vision` on both
|
||||||
the inbound client entry and the share link.
|
the inbound client entry and the share link.
|
||||||
|
- **Old client cores rejected by default.** An empty **Min Client Ver** is not
|
||||||
|
"no limit": Xray-core falls back to the built-in minimum of the core build you
|
||||||
|
run (26.3.27 in current releases) that keeps client TLS fingerprints fresh, so
|
||||||
|
third-party cores such as Mihomo and sing-box fail REALITY verification even
|
||||||
|
with a correct config — clients see timeouts while only Xray-core based apps
|
||||||
|
connect. Set it to `1.0.0` only if you must support them; that also re-admits
|
||||||
|
outdated fingerprints.
|
||||||
|
|
||||||
</Callout>
|
</Callout>
|
||||||
|
|
||||||
|
|||||||
@@ -1,66 +1,47 @@
|
|||||||
---
|
---
|
||||||
title: API Tokens
|
title: API Tokens
|
||||||
description: >-
|
description: 'Manage Bearer tokens used for programmatic auth (bots, central
|
||||||
Manage Bearer tokens used for programmatic auth (bots, central panels acting
|
panels acting on this node, CI). Each token has a unique name and an enabled
|
||||||
on this node, CI). Each token has a unique name and an enabled flag — disable
|
flag — disable to revoke without deleting, delete to revoke permanently.
|
||||||
to revoke without deleting, delete to revoke permanently. Tokens are stored as
|
Tokens are stored as SHA-256 hashes and the plaintext is returned only once,
|
||||||
SHA-256 hashes and the plaintext is returned only once, in the create response
|
in the create response — it cannot be retrieved afterwards, so copy it then.
|
||||||
— it cannot be retrieved afterwards, so copy it then. Send one as
|
Send one as <code>Authorization: Bearer <token></code> on any
|
||||||
<code>Authorization: Bearer <token></code> on any /panel/api/* request —
|
/panel/api/* request — the token is a full-admin credential.'
|
||||||
the token is a full-admin credential.
|
|
||||||
full: true
|
full: true
|
||||||
_openapi:
|
_openapi:
|
||||||
preload:
|
preload:
|
||||||
- ./public/openapi.json
|
- ./public/openapi.json
|
||||||
toc:
|
toc:
|
||||||
- depth: 2
|
- depth: 2
|
||||||
title: >-
|
title: List every API token, enabled or not. The token value is never returned —
|
||||||
List every API token, enabled or not. The token value is never returned
|
only metadata.
|
||||||
— only metadata.
|
url: '#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
|
- depth: 2
|
||||||
title: >-
|
title: Mint a scoped API token. The server-generated plaintext is returned only
|
||||||
Mint a new API token. Name must be unique and 1-64 characters; the token
|
once and stored as a hash.
|
||||||
string is server-generated and returned only in this response — it is
|
url: '#mint-a-scoped-api-token-the-server-generated-plaintext-is-returned-only-once-and-stored-as-a-hash'
|
||||||
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
|
|
||||||
- depth: 2
|
- depth: 2
|
||||||
title: >-
|
title: Permanently delete a token. Any caller using it stops authenticating
|
||||||
Permanently delete a token. Any caller using it stops authenticating
|
|
||||||
immediately.
|
immediately.
|
||||||
url: >-
|
url: '#permanently-delete-a-token-any-caller-using-it-stops-authenticating-immediately'
|
||||||
#permanently-delete-a-token-any-caller-using-it-stops-authenticating-immediately
|
|
||||||
- depth: 2
|
- depth: 2
|
||||||
title: >-
|
title: Toggle a token enabled/disabled without deleting it. Disabled tokens are
|
||||||
Toggle a token enabled/disabled without deleting it. Disabled tokens are
|
|
||||||
rejected by checkAPIAuth on the next request.
|
rejected by checkAPIAuth on the next request.
|
||||||
url: >-
|
url: '#toggle-a-token-enableddisabled-without-deleting-it-disabled-tokens-are-rejected-by-checkapiauth-on-the-next-request'
|
||||||
#toggle-a-token-enableddisabled-without-deleting-it-disabled-tokens-are-rejected-by-checkapiauth-on-the-next-request
|
|
||||||
structuredData:
|
structuredData:
|
||||||
headings:
|
headings:
|
||||||
- content: >-
|
- content: List every API token, enabled or not. The token value is never returned
|
||||||
List every API token, enabled or not. The token value is never
|
— only metadata.
|
||||||
returned — only metadata.
|
id: list-every-api-token-enabled-or-not-the-token-value-is-never-returned--only-metadata
|
||||||
id: >-
|
- content: Mint a scoped API token. The server-generated plaintext is returned
|
||||||
list-every-api-token-enabled-or-not-the-token-value-is-never-returned--only-metadata
|
only once and stored as a hash.
|
||||||
- content: >-
|
id: mint-a-scoped-api-token-the-server-generated-plaintext-is-returned-only-once-and-stored-as-a-hash
|
||||||
Mint a new API token. Name must be unique and 1-64 characters; the
|
- content: Permanently delete a token. Any caller using it stops authenticating
|
||||||
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
|
|
||||||
immediately.
|
immediately.
|
||||||
id: >-
|
id: permanently-delete-a-token-any-caller-using-it-stops-authenticating-immediately
|
||||||
permanently-delete-a-token-any-caller-using-it-stops-authenticating-immediately
|
- content: Toggle a token enabled/disabled without deleting it. Disabled tokens
|
||||||
- content: >-
|
|
||||||
Toggle a token enabled/disabled without deleting it. Disabled tokens
|
|
||||||
are rejected by checkAPIAuth on the next request.
|
are rejected by checkAPIAuth on the next request.
|
||||||
id: >-
|
id: toggle-a-token-enableddisabled-without-deleting-it-disabled-tokens-are-rejected-by-checkapiauth-on-the-next-request
|
||||||
toggle-a-token-enableddisabled-without-deleting-it-disabled-tokens-are-rejected-by-checkapiauth-on-the-next-request
|
|
||||||
contents: []
|
contents: []
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
---
|
---
|
||||||
title: Authentication
|
title: Authentication
|
||||||
description: >-
|
description: Two authentication modes are supported. UI sessions use a cookie
|
||||||
Two authentication modes are supported. UI sessions use a cookie set by the
|
set by the login endpoint. Programmatic clients (bots, scripts, remote panels)
|
||||||
login endpoint. Programmatic clients (bots, scripts, remote panels)
|
|
||||||
authenticate with a Bearer token taken from Settings → Security → API Token.
|
authenticate with a Bearer token taken from Settings → Security → API Token.
|
||||||
Both work for every endpoint under /panel/api/*.
|
Both work for every endpoint under /panel/api/*.
|
||||||
full: true
|
full: true
|
||||||
@@ -11,51 +10,38 @@ _openapi:
|
|||||||
- ./public/openapi.json
|
- ./public/openapi.json
|
||||||
toc:
|
toc:
|
||||||
- depth: 2
|
- depth: 2
|
||||||
title: >-
|
title: Authenticate with username + password and receive a session cookie.
|
||||||
Authenticate with username + password and receive a session cookie.
|
|
||||||
Required before any cookie-based API call.
|
Required before any cookie-based API call.
|
||||||
url: >-
|
url: '#authenticate-with-username--password-and-receive-a-session-cookie-required-before-any-cookie-based-api-call'
|
||||||
#authenticate-with-username--password-and-receive-a-session-cookie-required-before-any-cookie-based-api-call
|
|
||||||
- depth: 2
|
- depth: 2
|
||||||
title: Clear the session cookie. Requires the CSRF header for browser sessions.
|
title: Clear the session cookie. Requires the CSRF header for browser sessions.
|
||||||
url: '#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
|
- depth: 2
|
||||||
title: >-
|
title: Mint a CSRF token for the current session. The SPA replays it in the
|
||||||
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
|
X-CSRF-Token header on unsafe requests. Bearer-token callers can skip
|
||||||
this — the middleware short-circuits CSRF for authenticated API
|
this — the middleware short-circuits CSRF for authenticated API
|
||||||
requests.
|
requests.
|
||||||
url: >-
|
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'
|
||||||
#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
|
- depth: 2
|
||||||
title: >-
|
title: Returns whether 2FA is enabled on the panel — used by the login page to
|
||||||
Returns whether 2FA is enabled on the panel — used by the login page to
|
|
||||||
decide whether to show the OTP field.
|
decide whether to show the OTP field.
|
||||||
url: >-
|
url: '#returns-whether-2fa-is-enabled-on-the-panel--used-by-the-login-page-to-decide-whether-to-show-the-otp-field'
|
||||||
#returns-whether-2fa-is-enabled-on-the-panel--used-by-the-login-page-to-decide-whether-to-show-the-otp-field
|
|
||||||
structuredData:
|
structuredData:
|
||||||
headings:
|
headings:
|
||||||
- content: >-
|
- content: Authenticate with username + password and receive a session cookie.
|
||||||
Authenticate with username + password and receive a session cookie.
|
|
||||||
Required before any cookie-based API call.
|
Required before any cookie-based API call.
|
||||||
id: >-
|
id: authenticate-with-username--password-and-receive-a-session-cookie-required-before-any-cookie-based-api-call
|
||||||
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
|
||||||
- content: >-
|
|
||||||
Clear the session cookie. Requires the CSRF header for browser
|
|
||||||
sessions.
|
sessions.
|
||||||
id: clear-the-session-cookie-requires-the-csrf-header-for-browser-sessions
|
id: clear-the-session-cookie-requires-the-csrf-header-for-browser-sessions
|
||||||
- content: >-
|
- content: Mint a CSRF token for the current session. The SPA replays it in the
|
||||||
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
|
X-CSRF-Token header on unsafe requests. Bearer-token callers can skip
|
||||||
this — the middleware short-circuits CSRF for authenticated API
|
this — the middleware short-circuits CSRF for authenticated API
|
||||||
requests.
|
requests.
|
||||||
id: >-
|
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
|
||||||
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
|
||||||
- content: >-
|
decide whether to show the OTP field.
|
||||||
Returns whether 2FA is enabled on the panel — used by the login page
|
id: returns-whether-2fa-is-enabled-on-the-panel--used-by-the-login-page-to-decide-whether-to-show-the-otp-field
|
||||||
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: []
|
contents: []
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -7,18 +7,14 @@ _openapi:
|
|||||||
- ./public/openapi.json
|
- ./public/openapi.json
|
||||||
toc:
|
toc:
|
||||||
- depth: 2
|
- depth: 2
|
||||||
title: >-
|
title: Send a fresh DB backup to every Telegram chat configured as an admin
|
||||||
Send a fresh DB backup to every Telegram chat configured as an admin
|
|
||||||
recipient. No body, no params.
|
recipient. No body, no params.
|
||||||
url: >-
|
url: '#send-a-fresh-db-backup-to-every-telegram-chat-configured-as-an-admin-recipient-no-body-no-params'
|
||||||
#send-a-fresh-db-backup-to-every-telegram-chat-configured-as-an-admin-recipient-no-body-no-params
|
|
||||||
structuredData:
|
structuredData:
|
||||||
headings:
|
headings:
|
||||||
- content: >-
|
- content: Send a fresh DB backup to every Telegram chat configured as an admin
|
||||||
Send a fresh DB backup to every Telegram chat configured as an admin
|
|
||||||
recipient. No body, no params.
|
recipient. No body, no params.
|
||||||
id: >-
|
id: send-a-fresh-db-backup-to-every-telegram-chat-configured-as-an-admin-recipient-no-body-no-params
|
||||||
send-a-fresh-db-backup-to-every-telegram-chat-configured-as-an-admin-recipient-no-body-no-params
|
|
||||||
contents: []
|
contents: []
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -1,195 +1,150 @@
|
|||||||
---
|
---
|
||||||
title: Clients
|
title: Clients
|
||||||
description: >-
|
description: Manage clients as first-class entities that can be attached to one
|
||||||
Manage clients as first-class entities that can be attached to one or more
|
or more inbounds. A single client row drives the settings.clients entry in
|
||||||
inbounds. A single client row drives the settings.clients entry in every
|
every inbound it belongs to. Endpoints live under /panel/api/clients.
|
||||||
inbound it belongs to. Endpoints live under /panel/api/clients.
|
|
||||||
full: true
|
full: true
|
||||||
_openapi:
|
_openapi:
|
||||||
preload:
|
preload:
|
||||||
- ./public/openapi.json
|
- ./public/openapi.json
|
||||||
toc:
|
toc:
|
||||||
- depth: 2
|
- depth: 2
|
||||||
title: >-
|
title: List every client with its attached inbound IDs and traffic record. The
|
||||||
List every client with its attached inbound IDs and traffic record. The
|
|
||||||
reverse field, if set, is returned as a nested JSON object (legacy
|
reverse field, if set, is returned as a nested JSON object (legacy
|
||||||
JSON-encoded-string form is still accepted on write).
|
JSON-encoded-string form is still accepted on write).
|
||||||
url: >-
|
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'
|
||||||
#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
|
- depth: 2
|
||||||
title: >-
|
title: Filter, sort, and paginate clients on the server. Each item is a slim row
|
||||||
Filter, sort, and paginate clients on the server. Each item is a slim
|
(no uuid/password/auth/flow/security/reverse/tgId) so the clients page
|
||||||
row (no uuid/password/auth/flow/security/reverse/tgId) so the clients
|
can ship 25-ish rows in a few KB instead of the full table. The response
|
||||||
page can ship 25-ish rows in a few KB instead of the full table. The
|
also includes a summary computed across the full DB row set so dashboard
|
||||||
response also includes a summary computed across the full DB row set so
|
counters stay stable as the user paginates or filters. Page size capped
|
||||||
dashboard counters stay stable as the user paginates or filters. Page
|
at 200; fetch /get/:email to obtain the full per-client payload for an
|
||||||
size capped at 200; fetch /get/:email to obtain the full per-client
|
edit/info modal.
|
||||||
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'
|
||||||
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
|
|
||||||
- depth: 2
|
- depth: 2
|
||||||
title: >-
|
title: Fetch one client by email, including the inbound IDs and external config
|
||||||
Fetch one client by email, including the inbound IDs and external config
|
|
||||||
IDs it is attached to.
|
IDs it is attached to.
|
||||||
url: >-
|
url: '#fetch-one-client-by-email-including-the-inbound-ids-and-external-config-ids-it-is-attached-to'
|
||||||
#fetch-one-client-by-email-including-the-inbound-ids-and-external-config-ids-it-is-attached-to
|
|
||||||
- depth: 2
|
- depth: 2
|
||||||
title: >-
|
title: Create a new client and attach it to one or more inbounds in a single
|
||||||
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
|
call. Body is JSON. Per-protocol secrets (UUID for VLESS/VMess, password
|
||||||
for Trojan/Shadowsocks, auth for Hysteria) are generated server-side
|
for Trojan/Shadowsocks, auth for Hysteria) are generated server-side
|
||||||
when omitted, so callers can send only the universal fields.
|
when omitted, so callers can send only the universal fields.
|
||||||
url: >-
|
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-uuid-for-vlessvmess-password-for-trojanshadowsocks-auth-for-hysteria-are-generated-server-side-when-omitted-so-callers-can-send-only-the-universal-fields
|
|
||||||
- depth: 2
|
- depth: 2
|
||||||
title: >-
|
title: Update an existing client by email. Changes propagate to every attached
|
||||||
Update an existing client by email. Changes propagate to every attached
|
|
||||||
inbound. Body is the JSON client payload — supply the full set of fields
|
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).
|
you want to keep (the server replaces the row, it does not patch).
|
||||||
url: >-
|
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'
|
||||||
#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
|
- depth: 2
|
||||||
title: >-
|
title: Delete a client by email. Removes it from every attached inbound and
|
||||||
Delete a client by email. Removes it from every attached inbound and
|
|
||||||
drops its traffic record unless keepTraffic=1 is passed.
|
drops its traffic record unless keepTraffic=1 is passed.
|
||||||
url: >-
|
url: '#delete-a-client-by-email-removes-it-from-every-attached-inbound-and-drops-its-traffic-record-unless-keeptraffic1-is-passed'
|
||||||
#delete-a-client-by-email-removes-it-from-every-attached-inbound-and-drops-its-traffic-record-unless-keeptraffic1-is-passed
|
|
||||||
- depth: 2
|
- depth: 2
|
||||||
title: >-
|
title: Attach an existing client to one or more additional inbounds. Body is
|
||||||
Attach an existing client to one or more additional inbounds. Body is
|
|
||||||
JSON.
|
JSON.
|
||||||
url: >-
|
url: '#attach-an-existing-client-to-one-or-more-additional-inbounds-body-is-json'
|
||||||
#attach-an-existing-client-to-one-or-more-additional-inbounds-body-is-json
|
|
||||||
- depth: 2
|
- depth: 2
|
||||||
title: Detach a client from one or more inbounds without deleting the client.
|
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'
|
url: '#detach-a-client-from-one-or-more-inbounds-without-deleting-the-client'
|
||||||
- depth: 2
|
- depth: 2
|
||||||
title: >-
|
title: Replace a client's external links (per-client share links and remote
|
||||||
Replace a client's external links (per-client share links and remote
|
|
||||||
subscription URLs surfaced in their subscription). Sends the full set;
|
subscription URLs surfaced in their subscription). Sends the full set;
|
||||||
the server replaces all rows.
|
the server replaces all rows.
|
||||||
url: >-
|
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'
|
||||||
#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
|
|
||||||
- depth: 2
|
- depth: 2
|
||||||
title: >-
|
title: Reset the up/down counters for every client globally. Quotas and expiry
|
||||||
Reset the up/down counters for every client globally. Quotas and expiry
|
|
||||||
are not affected. Triggers an Xray restart if any counter actually
|
are not affected. Triggers an Xray restart if any counter actually
|
||||||
moved.
|
moved.
|
||||||
url: >-
|
url: '#reset-the-updown-counters-for-every-client-globally-quotas-and-expiry-are-not-affected-triggers-an-xray-restart-if-any-counter-actually-moved'
|
||||||
#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
|
- depth: 2
|
||||||
title: >-
|
title: Delete every client whose traffic quota is exhausted (used >= total, when
|
||||||
Delete every client whose traffic quota is exhausted (used >= total,
|
reset is disabled) or whose expiry has passed. Returns the deleted count
|
||||||
when reset is disabled) or whose expiry has passed. Returns the deleted
|
and triggers an Xray restart when any client was on a running inbound.
|
||||||
count and triggers an Xray restart when any client was on a running
|
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'
|
||||||
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
|
- depth: 2
|
||||||
title: >-
|
title: Delete every client that is not attached to any inbound, along with its
|
||||||
Delete every client that is not attached to any inbound, along with its
|
|
||||||
traffic record, IP log, and external links. Useful for clearing clients
|
traffic record, IP log, and external links. Useful for clearing clients
|
||||||
left unattached after their inbounds were removed. Returns the deleted
|
left unattached after their inbounds were removed. Returns the deleted
|
||||||
count. Cannot be undone.
|
count. Cannot be undone.
|
||||||
url: >-
|
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'
|
||||||
#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
|
|
||||||
- depth: 2
|
- depth: 2
|
||||||
title: >-
|
title: Return every client as a {client, inboundIds} array — the same shape
|
||||||
Return every client as a {client, inboundIds} array — the same shape
|
|
||||||
/bulkCreate and /import accept — so the payload round-trips straight
|
/bulkCreate and /import accept — so the payload round-trips straight
|
||||||
back through /import. Clients with no inbound attachment are included
|
back through /import. Clients with no inbound attachment are included
|
||||||
with an empty inboundIds list. The UI shows this in a CodeMirror viewer
|
with an empty inboundIds list. The UI shows this in a CodeMirror viewer
|
||||||
(copy / download); programmatic callers get the array in obj.
|
(copy / download); programmatic callers get the array in obj.
|
||||||
url: >-
|
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'
|
||||||
#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
|
- depth: 2
|
||||||
title: >-
|
title: 'Import clients from a JSON body { "data": "<json>" }, where data is a
|
||||||
Import clients from a JSON body { "data": "<json>" }, where data is a
|
|
||||||
string-encoded array produced by /export ([{client, inboundIds}]). Items
|
string-encoded array produced by /export ([{client, inboundIds}]). Items
|
||||||
with inboundIds are created and attached to those inbounds; items with
|
with inboundIds are created and attached to those inbounds; items with
|
||||||
an empty inboundIds list are restored as unattached client records.
|
an empty inboundIds list are restored as unattached client records.
|
||||||
Existing emails are never overwritten — they are returned in skipped.
|
Existing emails are never overwritten — they are returned in skipped.
|
||||||
Triggers a single Xray restart at the end if any target inbound was
|
Triggers a single Xray restart at the end if any target inbound was
|
||||||
running.
|
running.'
|
||||||
url: >-
|
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'
|
||||||
#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
|
- depth: 2
|
||||||
title: >-
|
title: 'Shift expiry and/or traffic quota for many clients in one call.
|
||||||
Shift expiry and/or traffic quota for many clients in one call.
|
|
||||||
addDays/addBytes may be negative. Clients with unlimited expiry
|
addDays/addBytes may be negative. Clients with unlimited expiry
|
||||||
(expiryTime=0) or unlimited traffic (totalGB=0) are skipped for the
|
(expiryTime=0) or unlimited traffic (totalGB=0) are skipped for the
|
||||||
corresponding field — bulk extend never converts unlimited to limited.
|
corresponding field — bulk extend never converts unlimited to limited.
|
||||||
The optional flow directive sets the XTLS flow on every client: "none"
|
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
|
clears it, "xtls-rprx-vision"/"xtls-rprx-vision-udp443" set it where the
|
||||||
inbound supports it (omit or "" to leave it unchanged). Returns the
|
inbound supports it (omit or "" to leave it unchanged). Returns the
|
||||||
adjusted count and per-email skip reasons.
|
adjusted count and per-email skip reasons.'
|
||||||
url: >-
|
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'
|
||||||
#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
|
|
||||||
- depth: 2
|
- depth: 2
|
||||||
title: >-
|
title: Enable many clients in one call. Emails are grouped by inbound and
|
||||||
Enable many clients in one call. Emails are grouped by inbound and
|
|
||||||
applied with a single read-modify-write per inbound; the running Xray
|
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
|
(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
|
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.
|
the flag — the traffic loop will disable it again on the next tick.
|
||||||
Returns the changed count and per-email skip reasons.
|
Returns the changed count and per-email skip reasons.
|
||||||
url: >-
|
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'
|
||||||
#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
|
- depth: 2
|
||||||
title: >-
|
title: Disable many clients in one call. Emails are grouped by inbound and
|
||||||
Disable many clients in one call. Emails are grouped by inbound and
|
|
||||||
applied with a single read-modify-write per inbound; the running Xray
|
applied with a single read-modify-write per inbound; the running Xray
|
||||||
(local or remote node) is updated to remove each user. Returns the
|
(local or remote node) is updated to remove each user. Returns the
|
||||||
changed count and per-email skip reasons.
|
changed count and per-email skip reasons.
|
||||||
url: >-
|
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'
|
||||||
#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
|
- depth: 2
|
||||||
title: >-
|
title: Delete many clients in one call. The server processes the list
|
||||||
Delete many clients in one call. The server processes the list
|
|
||||||
sequentially so each delete sees the committed state of the previous one
|
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
|
— 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.
|
keepTraffic=true to retain the xray_client_traffic rows after deletion.
|
||||||
url: >-
|
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'
|
||||||
#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
|
- depth: 2
|
||||||
title: >-
|
title: Create many clients in one call. Body is a JSON array of {client,
|
||||||
Create many clients in one call. Body is a JSON array of {client,
|
|
||||||
inboundIds} payloads — the same shape /add accepts. Items are processed
|
inboundIds} payloads — the same shape /add accepts. Items are processed
|
||||||
sequentially; per-email skip reasons are returned for items that fail
|
sequentially; per-email skip reasons are returned for items that fail
|
||||||
(e.g., duplicate email). Triggers a single Xray restart at the end if
|
(e.g., duplicate email). Triggers a single Xray restart at the end if
|
||||||
any inbound was running.
|
any inbound was running.
|
||||||
url: >-
|
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'
|
||||||
#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
|
- depth: 2
|
||||||
title: >-
|
title: Add many clients to a group in one call. Updates clients.group_name and
|
||||||
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
|
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
|
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
|
client_groups or as a derived label), it is auto-created as a persistent
|
||||||
group. To clear the group label, use /groups/bulkRemove instead.
|
group. To clear the group label, use /groups/bulkRemove instead.
|
||||||
url: >-
|
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'
|
||||||
#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
|
- depth: 2
|
||||||
title: >-
|
title: Clear the group label on many clients in one call. Inverse of
|
||||||
Clear the group label on many clients in one call. Inverse of
|
|
||||||
/groups/bulkAdd. Clients themselves are kept — only the group label is
|
/groups/bulkAdd. Clients themselves are kept — only the group label is
|
||||||
cleared from clients.group_name and from each owning inbound's settings
|
cleared from clients.group_name and from each owning inbound's settings
|
||||||
JSON. Groups become empty if all their members are removed.
|
JSON. Groups become empty if all their members are removed.
|
||||||
url: >-
|
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'
|
||||||
#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
|
- depth: 2
|
||||||
title: >-
|
title: Attach many existing clients to many inbounds in one call. Each client
|
||||||
Attach many existing clients to many inbounds in one call. Each client
|
|
||||||
keeps its identity (email/UUID/password/subId) and a shared traffic row;
|
keeps its identity (email/UUID/password/subId) and a shared traffic row;
|
||||||
all clients are added to a target inbound in a single AddInboundClient
|
all clients are added to a target inbound in a single AddInboundClient
|
||||||
call. Clients already present on a target are reported under skipped.
|
call. Clients already present on a target are reported under skipped.
|
||||||
Returns per-email attached/skipped/errors lists and triggers a single
|
Returns per-email attached/skipped/errors lists and triggers a single
|
||||||
Xray restart if any target inbound was running.
|
Xray restart if any target inbound was running.
|
||||||
url: >-
|
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'
|
||||||
#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
|
- depth: 2
|
||||||
title: >-
|
title: "Mirror of bulkAttach: detach many existing clients from many inbounds in
|
||||||
Mirror of bulkAttach: detach many existing clients from many inbounds in
|
|
||||||
one call. For each email, intersects the client's current inbounds with
|
one call. For each email, intersects the client's current inbounds with
|
||||||
the requested set and detaches from those only; (email, inbound) pairs
|
the requested set and detaches from those only; (email, inbound) pairs
|
||||||
where the client is not currently attached are silently no-ops. Emails
|
where the client is not currently attached are silently no-ops. Emails
|
||||||
@@ -197,110 +152,82 @@ _openapi:
|
|||||||
skipped. Client records are kept even if they become orphaned — use
|
skipped. Client records are kept even if they become orphaned — use
|
||||||
bulkDel for full removal. Returns per-email detached/skipped/errors
|
bulkDel for full removal. Returns per-email detached/skipped/errors
|
||||||
lists and triggers a single Xray restart if any target inbound was
|
lists and triggers a single Xray restart if any target inbound was
|
||||||
running.
|
running."
|
||||||
url: >-
|
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'
|
||||||
#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
|
- depth: 2
|
||||||
title: >-
|
title: Zero up/down counters for many clients in one call. Loops the
|
||||||
Zero up/down counters for many clients in one call. Loops the
|
|
||||||
single-reset path so each client is re-enabled across its attached
|
single-reset path so each client is re-enabled across its attached
|
||||||
inbounds and pushed to Xray/remote nodes. Returns the count of
|
inbounds and pushed to Xray/remote nodes. Returns the count of
|
||||||
successfully reset clients.
|
successfully reset clients.
|
||||||
url: >-
|
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'
|
||||||
#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
|
- depth: 2
|
||||||
title: >-
|
title: List all client groups with their member counts. Merges persisted groups
|
||||||
List all client groups with their member counts. Merges persisted groups
|
|
||||||
(rows in client_groups, including empty placeholders) with the distinct
|
(rows in client_groups, including empty placeholders) with the distinct
|
||||||
group_name values currently set on clients. Sorted alphabetically
|
group_name values currently set on clients. Sorted alphabetically
|
||||||
(case-insensitive).
|
(case-insensitive).
|
||||||
url: >-
|
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'
|
||||||
#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
|
- depth: 2
|
||||||
title: >-
|
title: Return just the email list of clients that currently belong to the given
|
||||||
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
|
group. Useful for fanning a single bulk action over an entire group
|
||||||
without round-tripping the full client list.
|
without round-tripping the full client list.
|
||||||
url: >-
|
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'
|
||||||
#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
|
- depth: 2
|
||||||
title: >-
|
title: Create a new empty (placeholder) group. The group becomes selectable in
|
||||||
Create a new empty (placeholder) group. The group becomes selectable in
|
|
||||||
client forms and the filter drawer even before any client is added to
|
client forms and the filter drawer even before any client is added to
|
||||||
it. Errors if a group with the same name already exists.
|
it. Errors if a group with the same name already exists.
|
||||||
url: >-
|
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'
|
||||||
#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
|
- depth: 2
|
||||||
title: >-
|
title: Rename a group. The new name is applied to the client_groups row AND
|
||||||
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
|
propagated to every matching client (both clients.group_name and the
|
||||||
client entry inside every owning inbound's settings JSON) in a single
|
client entry inside every owning inbound's settings JSON) in a single
|
||||||
transaction. Returns the number of clients whose label was updated.
|
transaction. Returns the number of clients whose label was updated.
|
||||||
url: >-
|
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'
|
||||||
#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
|
- depth: 2
|
||||||
title: >-
|
title: Remove a group. Deletes the client_groups row and clears the group label
|
||||||
Remove a group. Deletes the client_groups row and clears the group label
|
|
||||||
from every matching client (both clients.group_name and the inbound
|
from every matching client (both clients.group_name and the inbound
|
||||||
settings JSON). The clients themselves are NOT deleted — use /bulkDel
|
settings JSON). The clients themselves are NOT deleted — use /bulkDel
|
||||||
after filtering by group for that. Returns the count of clients whose
|
after filtering by group for that. Returns the count of clients whose
|
||||||
label was cleared.
|
label was cleared.
|
||||||
url: >-
|
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'
|
||||||
#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
|
- depth: 2
|
||||||
title: >-
|
title: Zero out a single client’s up/down counters. Re-enables the client across
|
||||||
Zero out a single client’s up/down counters. Re-enables the client
|
every attached inbound and pushes the change to Xray (or the remote
|
||||||
across every attached inbound and pushes the change to Xray (or the
|
node) so depleted users can connect again immediately.
|
||||||
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'
|
||||||
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
|
- depth: 2
|
||||||
title: >-
|
title: Manually adjust a client’s upload + download counters. Useful for
|
||||||
Manually adjust a client’s upload + download counters. Useful for
|
|
||||||
migrations from external accounting systems.
|
migrations from external accounting systems.
|
||||||
url: >-
|
url: '#manually-adjust-a-clients-upload--download-counters-useful-for-migrations-from-external-accounting-systems'
|
||||||
#manually-adjust-a-clients-upload--download-counters-useful-for-migrations-from-external-accounting-systems
|
|
||||||
- depth: 2
|
- depth: 2
|
||||||
title: >-
|
title: List source IPs that have connected with the given client’s credentials.
|
||||||
List source IPs that have connected with the given client’s credentials.
|
|
||||||
Returns an array of "ip (timestamp)" strings.
|
Returns an array of "ip (timestamp)" strings.
|
||||||
url: >-
|
url: '#list-source-ips-that-have-connected-with-the-given-clients-credentials-returns-an-array-of-ip-timestamp-strings'
|
||||||
#list-source-ips-that-have-connected-with-the-given-clients-credentials-returns-an-array-of-ip-timestamp-strings
|
|
||||||
- depth: 2
|
- depth: 2
|
||||||
title: Reset the recorded IP list for a client.
|
title: Reset the recorded IP list for a client.
|
||||||
url: '#reset-the-recorded-ip-list-for-a-client'
|
url: '#reset-the-recorded-ip-list-for-a-client'
|
||||||
- depth: 2
|
- depth: 2
|
||||||
title: >-
|
title: List the emails of currently connected clients (last seen within the
|
||||||
List the emails of currently connected clients (last seen within the
|
|
||||||
heartbeat window), deduped across every node.
|
heartbeat window), deduped across every node.
|
||||||
url: >-
|
url: '#list-the-emails-of-currently-connected-clients-last-seen-within-the-heartbeat-window-deduped-across-every-node'
|
||||||
#list-the-emails-of-currently-connected-clients-last-seen-within-the-heartbeat-window-deduped-across-every-node
|
|
||||||
- depth: 2
|
- depth: 2
|
||||||
title: >-
|
title: Online client emails grouped by the panelGuid of the node that physically
|
||||||
Online client emails grouped by the panelGuid of the node that
|
hosts each client. The local panel uses its own GUID; each node (at any
|
||||||
physically hosts each client. The local panel uses its own GUID; each
|
depth in a chain) uses its GUID. Lets the inbounds page attribute online
|
||||||
node (at any depth in a chain) uses its GUID. Lets the inbounds page
|
status to the real node instead of the intermediate one it syncs
|
||||||
attribute online status to the real node instead of the intermediate one
|
through.
|
||||||
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'
|
||||||
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
|
- depth: 2
|
||||||
title: >-
|
title: Per-client source IPs grouped by the panelGuid of the node that observed
|
||||||
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
|
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
|
using the real visitor IPs each node sees, instead of the address of the
|
||||||
intermediate panel it syncs through.
|
intermediate panel it syncs through.
|
||||||
url: >-
|
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'
|
||||||
#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
|
- depth: 2
|
||||||
title: >-
|
title: Inbound tags that carried traffic within the heartbeat window, grouped by
|
||||||
Inbound tags that carried traffic within the heartbeat window, grouped
|
the hosting node's panelGuid. Pairs with onlinesByGuid so the inbounds
|
||||||
by the hosting node's panelGuid. Pairs with onlinesByGuid so the
|
page only marks a multi-inbound client online on the inbounds it
|
||||||
inbounds page only marks a multi-inbound client online on the inbounds
|
actually used. Nodes that do not report per-inbound activity are absent.
|
||||||
it actually used. Nodes that do not report per-inbound activity are
|
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'
|
||||||
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
|
- depth: 2
|
||||||
title: Map of client email → last-seen unix timestamp.
|
title: Map of client email → last-seen unix timestamp.
|
||||||
url: '#map-of-client-email--last-seen-unix-timestamp'
|
url: '#map-of-client-email--last-seen-unix-timestamp'
|
||||||
@@ -308,189 +235,142 @@ _openapi:
|
|||||||
title: Traffic counters for a client identified by email.
|
title: Traffic counters for a client identified by email.
|
||||||
url: '#traffic-counters-for-a-client-identified-by-email'
|
url: '#traffic-counters-for-a-client-identified-by-email'
|
||||||
- depth: 2
|
- depth: 2
|
||||||
title: >-
|
title: Return every protocol URL (vless://, vmess://, trojan://, ss://,
|
||||||
Return every protocol URL (vless://, vmess://, trojan://, ss://,
|
|
||||||
hysteria://, hy2://) for clients matching the subscription ID. Same
|
hysteria://, hy2://) for clients matching the subscription ID. Same
|
||||||
result set as /sub/<subId>, but as a JSON array — no base64. When an
|
result set as /sub/<subId>, but as a JSON array — no base64. When an
|
||||||
inbound has streamSettings.externalProxy set, one URL is emitted per
|
inbound has streamSettings.externalProxy set, one URL is emitted per
|
||||||
external proxy. Empty array when the subId has no enabled clients.
|
external proxy. Empty array when the subId has no enabled clients.
|
||||||
url: >-
|
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'
|
||||||
#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
|
- depth: 2
|
||||||
title: >-
|
title: 'Return every URL for one client across all attached inbounds — the same
|
||||||
Return every URL for one client across all attached inbounds — the same
|
|
||||||
strings the Copy URL button copies in the panel UI. Supported protocols:
|
strings the Copy URL button copies in the panel UI. Supported protocols:
|
||||||
vmess, vless, trojan, shadowsocks, hysteria. If
|
vmess, vless, trojan, shadowsocks, hysteria. If
|
||||||
streamSettings.externalProxy is set, returns one URL per external proxy.
|
streamSettings.externalProxy is set, returns one URL per external proxy.
|
||||||
Protocols without a URL form (socks, http, mixed, wireguard, dokodemo,
|
Protocols without a URL form (socks, http, mixed, wireguard, dokodemo,
|
||||||
tunnel) contribute nothing.
|
tunnel) contribute nothing.'
|
||||||
url: >-
|
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'
|
||||||
#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:
|
structuredData:
|
||||||
headings:
|
headings:
|
||||||
- content: >-
|
- content: List every client with its attached inbound IDs and traffic record. The
|
||||||
List every client with its attached inbound IDs and traffic record.
|
reverse field, if set, is returned as a nested JSON object (legacy
|
||||||
The reverse field, if set, is returned as a nested JSON object (legacy
|
|
||||||
JSON-encoded-string form is still accepted on write).
|
JSON-encoded-string form is still accepted on write).
|
||||||
id: >-
|
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
|
||||||
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
|
||||||
- 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
|
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
|
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
|
response also includes a summary computed across the full DB row set
|
||||||
so dashboard counters stay stable as the user paginates or filters.
|
so dashboard counters stay stable as the user paginates or filters.
|
||||||
Page size capped at 200; fetch /get/:email to obtain the full
|
Page size capped at 200; fetch /get/:email to obtain the full
|
||||||
per-client payload for an edit/info modal.
|
per-client payload for an edit/info modal.
|
||||||
id: >-
|
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
|
||||||
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
|
||||||
- content: >-
|
|
||||||
Fetch one client by email, including the inbound IDs and external
|
|
||||||
config IDs it is attached to.
|
config IDs it is attached to.
|
||||||
id: >-
|
id: fetch-one-client-by-email-including-the-inbound-ids-and-external-config-ids-it-is-attached-to
|
||||||
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
|
||||||
- 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,
|
call. Body is JSON. Per-protocol secrets (UUID for VLESS/VMess,
|
||||||
password for Trojan/Shadowsocks, auth for Hysteria) are generated
|
password for Trojan/Shadowsocks, auth for Hysteria) are generated
|
||||||
server-side when omitted, so callers can send only the universal
|
server-side when omitted, so callers can send only the universal
|
||||||
fields.
|
fields.
|
||||||
id: >-
|
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-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
|
||||||
- content: >-
|
inbound. Body is the JSON client payload — supply the full set of
|
||||||
Update an existing client by email. Changes propagate to every
|
fields you want to keep (the server replaces the row, it does not
|
||||||
attached inbound. Body is the JSON client payload — supply the full
|
patch).
|
||||||
set of fields you want to keep (the server replaces the row, it does
|
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
|
||||||
not patch).
|
- content: Delete a client by email. Removes it from every attached inbound and
|
||||||
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.
|
drops its traffic record unless keepTraffic=1 is passed.
|
||||||
id: >-
|
id: delete-a-client-by-email-removes-it-from-every-attached-inbound-and-drops-its-traffic-record-unless-keeptraffic1-is-passed
|
||||||
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
|
||||||
- content: >-
|
|
||||||
Attach an existing client to one or more additional inbounds. Body is
|
|
||||||
JSON.
|
JSON.
|
||||||
id: >-
|
id: attach-an-existing-client-to-one-or-more-additional-inbounds-body-is-json
|
||||||
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.
|
- 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
|
id: detach-a-client-from-one-or-more-inbounds-without-deleting-the-client
|
||||||
- content: >-
|
- content: Replace a client's external links (per-client share links and remote
|
||||||
Replace a client's external links (per-client share links and remote
|
|
||||||
subscription URLs surfaced in their subscription). Sends the full set;
|
subscription URLs surfaced in their subscription). Sends the full set;
|
||||||
the server replaces all rows.
|
the server replaces all rows.
|
||||||
id: >-
|
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
|
||||||
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
|
||||||
- content: >-
|
are not affected. Triggers an Xray restart if any counter actually
|
||||||
Reset the up/down counters for every client globally. Quotas and
|
moved.
|
||||||
expiry are not affected. Triggers an Xray restart if any counter
|
id: reset-the-updown-counters-for-every-client-globally-quotas-and-expiry-are-not-affected-triggers-an-xray-restart-if-any-counter-actually-moved
|
||||||
actually moved.
|
- content: Delete every client whose traffic quota is exhausted (used >= total,
|
||||||
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
|
when reset is disabled) or whose expiry has passed. Returns the
|
||||||
deleted count and triggers an Xray restart when any client was on a
|
deleted count and triggers an Xray restart when any client was on a
|
||||||
running inbound.
|
running inbound.
|
||||||
id: >-
|
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
|
||||||
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
|
||||||
- content: >-
|
traffic record, IP log, and external links. Useful for clearing
|
||||||
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
|
clients left unattached after their inbounds were removed. Returns the
|
||||||
deleted count. Cannot be undone.
|
deleted count. Cannot be undone.
|
||||||
id: >-
|
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
|
||||||
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
|
||||||
- content: >-
|
|
||||||
Return every client as a {client, inboundIds} array — the same shape
|
|
||||||
/bulkCreate and /import accept — so the payload round-trips straight
|
/bulkCreate and /import accept — so the payload round-trips straight
|
||||||
back through /import. Clients with no inbound attachment are included
|
back through /import. Clients with no inbound attachment are included
|
||||||
with an empty inboundIds list. The UI shows this in a CodeMirror
|
with an empty inboundIds list. The UI shows this in a CodeMirror
|
||||||
viewer (copy / download); programmatic callers get the array in obj.
|
viewer (copy / download); programmatic callers get the array in obj.
|
||||||
id: >-
|
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
|
||||||
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
|
||||||
- content: >-
|
|
||||||
Import clients from a JSON body { "data": "<json>" }, where data is a
|
|
||||||
string-encoded array produced by /export ([{client, inboundIds}]).
|
string-encoded array produced by /export ([{client, inboundIds}]).
|
||||||
Items with inboundIds are created and attached to those inbounds;
|
Items with inboundIds are created and attached to those inbounds;
|
||||||
items with an empty inboundIds list are restored as unattached client
|
items with an empty inboundIds list are restored as unattached client
|
||||||
records. Existing emails are never overwritten — they are returned in
|
records. Existing emails are never overwritten — they are returned in
|
||||||
skipped. Triggers a single Xray restart at the end if any target
|
skipped. Triggers a single Xray restart at the end if any target
|
||||||
inbound was running.
|
inbound was running.'
|
||||||
id: >-
|
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
|
||||||
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.
|
||||||
- content: >-
|
|
||||||
Shift expiry and/or traffic quota for many clients in one call.
|
|
||||||
addDays/addBytes may be negative. Clients with unlimited expiry
|
addDays/addBytes may be negative. Clients with unlimited expiry
|
||||||
(expiryTime=0) or unlimited traffic (totalGB=0) are skipped for the
|
(expiryTime=0) or unlimited traffic (totalGB=0) are skipped for the
|
||||||
corresponding field — bulk extend never converts unlimited to limited.
|
corresponding field — bulk extend never converts unlimited to limited.
|
||||||
The optional flow directive sets the XTLS flow on every client: "none"
|
The optional flow directive sets the XTLS flow on every client: "none"
|
||||||
clears it, "xtls-rprx-vision"/"xtls-rprx-vision-udp443" set it where
|
clears it, "xtls-rprx-vision"/"xtls-rprx-vision-udp443" set it where
|
||||||
the inbound supports it (omit or "" to leave it unchanged). Returns
|
the inbound supports it (omit or "" to leave it unchanged). Returns
|
||||||
the adjusted count and per-email skip reasons.
|
the adjusted count and per-email skip reasons.'
|
||||||
id: >-
|
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
|
||||||
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
|
||||||
- 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
|
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
|
(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
|
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
|
flips the flag — the traffic loop will disable it again on the next
|
||||||
tick. Returns the changed count and per-email skip reasons.
|
tick. Returns the changed count and per-email skip reasons.
|
||||||
id: >-
|
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
|
||||||
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
|
||||||
- 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
|
applied with a single read-modify-write per inbound; the running Xray
|
||||||
(local or remote node) is updated to remove each user. Returns the
|
(local or remote node) is updated to remove each user. Returns the
|
||||||
changed count and per-email skip reasons.
|
changed count and per-email skip reasons.
|
||||||
id: >-
|
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
|
||||||
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
|
||||||
- content: >-
|
|
||||||
Delete many clients in one call. The server processes the list
|
|
||||||
sequentially so each delete sees the committed state of the previous
|
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.
|
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
|
Pass keepTraffic=true to retain the xray_client_traffic rows after
|
||||||
deletion.
|
deletion.
|
||||||
id: >-
|
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
|
||||||
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,
|
||||||
- content: >-
|
|
||||||
Create many clients in one call. Body is a JSON array of {client,
|
|
||||||
inboundIds} payloads — the same shape /add accepts. Items are
|
inboundIds} payloads — the same shape /add accepts. Items are
|
||||||
processed sequentially; per-email skip reasons are returned for items
|
processed sequentially; per-email skip reasons are returned for items
|
||||||
that fail (e.g., duplicate email). Triggers a single Xray restart at
|
that fail (e.g., duplicate email). Triggers a single Xray restart at
|
||||||
the end if any inbound was running.
|
the end if any inbound was running.
|
||||||
id: >-
|
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
|
||||||
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
|
||||||
- content: >-
|
patches the matching client entry inside every owning inbound's
|
||||||
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
|
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
|
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
|
a persistent group. To clear the group label, use /groups/bulkRemove
|
||||||
instead.
|
instead.
|
||||||
id: >-
|
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
|
||||||
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
|
||||||
- content: >-
|
|
||||||
Clear the group label on many clients in one call. Inverse of
|
|
||||||
/groups/bulkAdd. Clients themselves are kept — only the group label is
|
/groups/bulkAdd. Clients themselves are kept — only the group label is
|
||||||
cleared from clients.group_name and from each owning inbound's
|
cleared from clients.group_name and from each owning inbound's
|
||||||
settings JSON. Groups become empty if all their members are removed.
|
settings JSON. Groups become empty if all their members are removed.
|
||||||
id: >-
|
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
|
||||||
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
|
||||||
- content: >-
|
|
||||||
Attach many existing clients to many inbounds in one call. Each client
|
|
||||||
keeps its identity (email/UUID/password/subId) and a shared traffic
|
keeps its identity (email/UUID/password/subId) and a shared traffic
|
||||||
row; all clients are added to a target inbound in a single
|
row; all clients are added to a target inbound in a single
|
||||||
AddInboundClient call. Clients already present on a target are
|
AddInboundClient call. Clients already present on a target are
|
||||||
reported under skipped. Returns per-email attached/skipped/errors
|
reported under skipped. Returns per-email attached/skipped/errors
|
||||||
lists and triggers a single Xray restart if any target inbound was
|
lists and triggers a single Xray restart if any target inbound was
|
||||||
running.
|
running.
|
||||||
id: >-
|
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
|
||||||
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
|
||||||
- content: >-
|
|
||||||
Mirror of bulkAttach: detach many existing clients from many inbounds
|
|
||||||
in one call. For each email, intersects the client's current inbounds
|
in one call. For each email, intersects the client's current inbounds
|
||||||
with the requested set and detaches from those only; (email, inbound)
|
with the requested set and detaches from those only; (email, inbound)
|
||||||
pairs where the client is not currently attached are silently no-ops.
|
pairs where the client is not currently attached are silently no-ops.
|
||||||
@@ -498,117 +378,86 @@ _openapi:
|
|||||||
under skipped. Client records are kept even if they become orphaned —
|
under skipped. Client records are kept even if they become orphaned —
|
||||||
use bulkDel for full removal. Returns per-email
|
use bulkDel for full removal. Returns per-email
|
||||||
detached/skipped/errors lists and triggers a single Xray restart if
|
detached/skipped/errors lists and triggers a single Xray restart if
|
||||||
any target inbound was running.
|
any target inbound was running."
|
||||||
id: >-
|
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
|
||||||
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
|
||||||
- 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
|
single-reset path so each client is re-enabled across its attached
|
||||||
inbounds and pushed to Xray/remote nodes. Returns the count of
|
inbounds and pushed to Xray/remote nodes. Returns the count of
|
||||||
successfully reset clients.
|
successfully reset clients.
|
||||||
id: >-
|
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
|
||||||
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
|
||||||
- content: >-
|
|
||||||
List all client groups with their member counts. Merges persisted
|
|
||||||
groups (rows in client_groups, including empty placeholders) with the
|
groups (rows in client_groups, including empty placeholders) with the
|
||||||
distinct group_name values currently set on clients. Sorted
|
distinct group_name values currently set on clients. Sorted
|
||||||
alphabetically (case-insensitive).
|
alphabetically (case-insensitive).
|
||||||
id: >-
|
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
|
||||||
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
|
||||||
- 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
|
given group. Useful for fanning a single bulk action over an entire
|
||||||
group without round-tripping the full client list.
|
group without round-tripping the full client list.
|
||||||
id: >-
|
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
|
||||||
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
|
||||||
- content: >-
|
client forms and the filter drawer even before any client is added to
|
||||||
Create a new empty (placeholder) group. The group becomes selectable
|
it. Errors if a group with the same name already exists.
|
||||||
in client forms and the filter drawer even before any client is added
|
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
|
||||||
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: >-
|
|
||||||
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
|
propagated to every matching client (both clients.group_name and the
|
||||||
client entry inside every owning inbound's settings JSON) in a single
|
client entry inside every owning inbound's settings JSON) in a single
|
||||||
transaction. Returns the number of clients whose label was updated.
|
transaction. Returns the number of clients whose label was updated.
|
||||||
id: >-
|
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
|
||||||
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
|
||||||
- content: >-
|
|
||||||
Remove a group. Deletes the client_groups row and clears the group
|
|
||||||
label from every matching client (both clients.group_name and the
|
label from every matching client (both clients.group_name and the
|
||||||
inbound settings JSON). The clients themselves are NOT deleted — use
|
inbound settings JSON). The clients themselves are NOT deleted — use
|
||||||
/bulkDel after filtering by group for that. Returns the count of
|
/bulkDel after filtering by group for that. Returns the count of
|
||||||
clients whose label was cleared.
|
clients whose label was cleared.
|
||||||
id: >-
|
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
|
||||||
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
|
||||||
- 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
|
across every attached inbound and pushes the change to Xray (or the
|
||||||
remote node) so depleted users can connect again immediately.
|
remote node) so depleted users can connect again immediately.
|
||||||
id: >-
|
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
|
||||||
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
|
||||||
- content: >-
|
|
||||||
Manually adjust a client’s upload + download counters. Useful for
|
|
||||||
migrations from external accounting systems.
|
migrations from external accounting systems.
|
||||||
id: >-
|
id: manually-adjust-a-clients-upload--download-counters-useful-for-migrations-from-external-accounting-systems
|
||||||
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
|
||||||
- content: >-
|
|
||||||
List source IPs that have connected with the given client’s
|
|
||||||
credentials. Returns an array of "ip (timestamp)" strings.
|
credentials. Returns an array of "ip (timestamp)" strings.
|
||||||
id: >-
|
id: list-source-ips-that-have-connected-with-the-given-clients-credentials-returns-an-array-of-ip-timestamp-strings
|
||||||
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.
|
- content: Reset the recorded IP list for a client.
|
||||||
id: reset-the-recorded-ip-list-for-a-client
|
id: reset-the-recorded-ip-list-for-a-client
|
||||||
- content: >-
|
- content: List the emails of currently connected clients (last seen within the
|
||||||
List the emails of currently connected clients (last seen within the
|
|
||||||
heartbeat window), deduped across every node.
|
heartbeat window), deduped across every node.
|
||||||
id: >-
|
id: list-the-emails-of-currently-connected-clients-last-seen-within-the-heartbeat-window-deduped-across-every-node
|
||||||
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
|
||||||
- content: >-
|
|
||||||
Online client emails grouped by the panelGuid of the node that
|
|
||||||
physically hosts each client. The local panel uses its own GUID; each
|
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
|
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
|
attribute online status to the real node instead of the intermediate
|
||||||
one it syncs through.
|
one it syncs through.
|
||||||
id: >-
|
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
|
||||||
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
|
||||||
- content: >-
|
|
||||||
Per-client source IPs grouped by the panelGuid of the node that
|
|
||||||
observed them. Lets the central panel attribute and enforce per-client
|
observed them. Lets the central panel attribute and enforce per-client
|
||||||
IP limits using the real visitor IPs each node sees, instead of the
|
IP limits using the real visitor IPs each node sees, instead of the
|
||||||
address of the intermediate panel it syncs through.
|
address of the intermediate panel it syncs through.
|
||||||
id: >-
|
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
|
||||||
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
|
||||||
- content: >-
|
|
||||||
Inbound tags that carried traffic within the heartbeat window, grouped
|
|
||||||
by the hosting node's panelGuid. Pairs with onlinesByGuid so the
|
by the hosting node's panelGuid. Pairs with onlinesByGuid so the
|
||||||
inbounds page only marks a multi-inbound client online on the inbounds
|
inbounds page only marks a multi-inbound client online on the inbounds
|
||||||
it actually used. Nodes that do not report per-inbound activity are
|
it actually used. Nodes that do not report per-inbound activity are
|
||||||
absent.
|
absent.
|
||||||
id: >-
|
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
|
||||||
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.
|
- content: Map of client email → last-seen unix timestamp.
|
||||||
id: 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.
|
- content: Traffic counters for a client identified by email.
|
||||||
id: traffic-counters-for-a-client-identified-by-email
|
id: traffic-counters-for-a-client-identified-by-email
|
||||||
- content: >-
|
- content: Return every protocol URL (vless://, vmess://, trojan://, ss://,
|
||||||
Return every protocol URL (vless://, vmess://, trojan://, ss://,
|
|
||||||
hysteria://, hy2://) for clients matching the subscription ID. Same
|
hysteria://, hy2://) for clients matching the subscription ID. Same
|
||||||
result set as /sub/<subId>, but as a JSON array — no base64. When an
|
result set as /sub/<subId>, but as a JSON array — no base64. When an
|
||||||
inbound has streamSettings.externalProxy set, one URL is emitted per
|
inbound has streamSettings.externalProxy set, one URL is emitted per
|
||||||
external proxy. Empty array when the subId has no enabled clients.
|
external proxy. Empty array when the subId has no enabled clients.
|
||||||
id: >-
|
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
|
||||||
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
|
||||||
- content: >-
|
|
||||||
Return every URL for one client across all attached inbounds — the
|
|
||||||
same strings the Copy URL button copies in the panel UI. Supported
|
same strings the Copy URL button copies in the panel UI. Supported
|
||||||
protocols: vmess, vless, trojan, shadowsocks, hysteria. If
|
protocols: vmess, vless, trojan, shadowsocks, hysteria. If
|
||||||
streamSettings.externalProxy is set, returns one URL per external
|
streamSettings.externalProxy is set, returns one URL per external
|
||||||
proxy. Protocols without a URL form (socks, http, mixed, wireguard,
|
proxy. Protocols without a URL form (socks, http, mixed, wireguard,
|
||||||
dokodemo, tunnel) contribute nothing.
|
dokodemo, tunnel) contribute nothing.'
|
||||||
id: >-
|
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
|
||||||
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: []
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
---
|
---
|
||||||
title: Hosts
|
title: Hosts
|
||||||
description: >-
|
description: Per-inbound override endpoints. Each enabled host renders one extra
|
||||||
Per-inbound override endpoints. Each enabled host renders one extra
|
|
||||||
subscription link/proxy with its own address/port/TLS, superseding the legacy
|
subscription link/proxy with its own address/port/TLS, superseding the legacy
|
||||||
externalProxy array. All endpoints under /panel/api/hosts.
|
externalProxy array. All endpoints under /panel/api/hosts.
|
||||||
full: true
|
full: true
|
||||||
@@ -10,11 +9,9 @@ _openapi:
|
|||||||
- ./public/openapi.json
|
- ./public/openapi.json
|
||||||
toc:
|
toc:
|
||||||
- depth: 2
|
- depth: 2
|
||||||
title: >-
|
title: List every host across all inbounds, grouped by inbound then ordered by
|
||||||
List every host across all inbounds, grouped by inbound then ordered by
|
|
||||||
sort order.
|
sort order.
|
||||||
url: >-
|
url: '#list-every-host-across-all-inbounds-grouped-by-inbound-then-ordered-by-sort-order'
|
||||||
#list-every-host-across-all-inbounds-grouped-by-inbound-then-ordered-by-sort-order
|
|
||||||
- depth: 2
|
- depth: 2
|
||||||
title: Fetch a single host by ID.
|
title: Fetch a single host by ID.
|
||||||
url: '#fetch-a-single-host-by-id'
|
url: '#fetch-a-single-host-by-id'
|
||||||
@@ -25,26 +22,20 @@ _openapi:
|
|||||||
title: Distinct, sorted set of tags used across all hosts.
|
title: Distinct, sorted set of tags used across all hosts.
|
||||||
url: '#distinct-sorted-set-of-tags-used-across-all-hosts'
|
url: '#distinct-sorted-set-of-tags-used-across-all-hosts'
|
||||||
- depth: 2
|
- depth: 2
|
||||||
title: >-
|
title: Create a host on an inbound. inboundId and remark are required; security
|
||||||
Create a host on an inbound. inboundId and remark are required; security
|
|
||||||
defaults to "same" (inherit the inbound).
|
defaults to "same" (inherit the inbound).
|
||||||
url: >-
|
url: '#create-a-host-on-an-inbound-inboundid-and-remark-are-required-security-defaults-to-same-inherit-the-inbound'
|
||||||
#create-a-host-on-an-inbound-inboundid-and-remark-are-required-security-defaults-to-same-inherit-the-inbound
|
|
||||||
- depth: 2
|
- depth: 2
|
||||||
title: >-
|
title: Replace a host’s content. The inbound and sort order are immutable here
|
||||||
Replace a host’s content. The inbound and sort order are immutable here
|
|
||||||
(use /reorder for ordering).
|
(use /reorder for ordering).
|
||||||
url: >-
|
url: '#replace-a-hosts-content-the-inbound-and-sort-order-are-immutable-here-use-reorder-for-ordering'
|
||||||
#replace-a-hosts-content-the-inbound-and-sort-order-are-immutable-here-use-reorder-for-ordering
|
|
||||||
- depth: 2
|
- depth: 2
|
||||||
title: Delete a host.
|
title: Delete a host.
|
||||||
url: '#delete-a-host'
|
url: '#delete-a-host'
|
||||||
- depth: 2
|
- depth: 2
|
||||||
title: >-
|
title: Enable or disable a single host (disabled hosts are skipped in
|
||||||
Enable or disable a single host (disabled hosts are skipped in
|
|
||||||
subscriptions).
|
subscriptions).
|
||||||
url: >-
|
url: '#enable-or-disable-a-single-host-disabled-hosts-are-skipped-in-subscriptions'
|
||||||
#enable-or-disable-a-single-host-disabled-hosts-are-skipped-in-subscriptions
|
|
||||||
- depth: 2
|
- depth: 2
|
||||||
title: Set host sort order by the position of each id in the array.
|
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'
|
url: '#set-host-sort-order-by-the-position-of-each-id-in-the-array'
|
||||||
@@ -56,34 +47,26 @@ _openapi:
|
|||||||
url: '#delete-many-hosts-in-one-call'
|
url: '#delete-many-hosts-in-one-call'
|
||||||
structuredData:
|
structuredData:
|
||||||
headings:
|
headings:
|
||||||
- content: >-
|
- content: List every host across all inbounds, grouped by inbound then ordered by
|
||||||
List every host across all inbounds, grouped by inbound then ordered
|
sort order.
|
||||||
by sort order.
|
id: 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.
|
- content: Fetch a single host by ID.
|
||||||
id: 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.
|
- content: Fetch one inbound's hosts, ordered by sort order then id.
|
||||||
id: fetch-one-inbounds-hosts-ordered-by-sort-order-then-id
|
id: fetch-one-inbounds-hosts-ordered-by-sort-order-then-id
|
||||||
- content: Distinct, sorted set of tags used across all hosts.
|
- content: Distinct, sorted set of tags used across all hosts.
|
||||||
id: distinct-sorted-set-of-tags-used-across-all-hosts
|
id: distinct-sorted-set-of-tags-used-across-all-hosts
|
||||||
- content: >-
|
- content: Create a host on an inbound. inboundId and remark are required;
|
||||||
Create a host on an inbound. inboundId and remark are required;
|
|
||||||
security defaults to "same" (inherit the inbound).
|
security defaults to "same" (inherit the inbound).
|
||||||
id: >-
|
id: create-a-host-on-an-inbound-inboundid-and-remark-are-required-security-defaults-to-same-inherit-the-inbound
|
||||||
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
|
||||||
- content: >-
|
(use /reorder for ordering).
|
||||||
Replace a host’s content. The inbound and sort order are immutable
|
id: replace-a-hosts-content-the-inbound-and-sort-order-are-immutable-here-use-reorder-for-ordering
|
||||||
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.
|
- content: Delete a host.
|
||||||
id: delete-a-host
|
id: delete-a-host
|
||||||
- content: >-
|
- content: Enable or disable a single host (disabled hosts are skipped in
|
||||||
Enable or disable a single host (disabled hosts are skipped in
|
|
||||||
subscriptions).
|
subscriptions).
|
||||||
id: >-
|
id: enable-or-disable-a-single-host-disabled-hosts-are-skipped-in-subscriptions
|
||||||
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.
|
- 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
|
id: set-host-sort-order-by-the-position-of-each-id-in-the-array
|
||||||
- content: Enable or disable many hosts in one call.
|
- content: Enable or disable many hosts in one call.
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
---
|
---
|
||||||
title: Inbounds
|
title: Inbounds
|
||||||
description: >-
|
description: Manage inbound configurations and their clients. All endpoints live
|
||||||
Manage inbound configurations and their clients. All endpoints live under
|
under /panel/api/inbounds and require a logged-in session or Bearer token.
|
||||||
/panel/api/inbounds and require a logged-in session or Bearer token.
|
|
||||||
Link-generating endpoints honour forwarded headers only when the request comes
|
Link-generating endpoints honour forwarded headers only when the request comes
|
||||||
from a configured trusted proxy.
|
from a configured trusted proxy.
|
||||||
full: true
|
full: true
|
||||||
@@ -11,25 +10,20 @@ _openapi:
|
|||||||
- ./public/openapi.json
|
- ./public/openapi.json
|
||||||
toc:
|
toc:
|
||||||
- depth: 2
|
- depth: 2
|
||||||
title: >-
|
title: List every inbound owned by the authenticated user, including each
|
||||||
List every inbound owned by the authenticated user, including each
|
|
||||||
inbound’s clientStats traffic counters. settings, streamSettings, and
|
inbound’s clientStats traffic counters. settings, streamSettings, and
|
||||||
sniffing are returned as nested JSON objects (no escaped strings);
|
sniffing are returned as nested JSON objects (no escaped strings);
|
||||||
legacy callers that send them back as JSON-encoded strings are still
|
legacy callers that send them back as JSON-encoded strings are still
|
||||||
accepted on write.
|
accepted on write.
|
||||||
url: >-
|
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'
|
||||||
#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
|
- depth: 2
|
||||||
title: >-
|
title: Same shape as /list but with settings.clients[] stripped down to {email,
|
||||||
Same shape as /list but with settings.clients[] stripped down to {email,
|
|
||||||
enable, comment} and ClientStats not enriched with UUID/SubId. Use this
|
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
|
for list pages; fetch /get/:id when you need the full per-client payload
|
||||||
(uuid, password, flow, ...).
|
(uuid, password, flow, ...).
|
||||||
url: >-
|
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-'
|
||||||
#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
|
- depth: 2
|
||||||
title: >-
|
title: Lightweight picker projection of the authenticated user’s inbounds.
|
||||||
Lightweight picker projection of the authenticated user’s inbounds.
|
|
||||||
Returns id, remark, tag, protocol, port, a server-computed
|
Returns id, remark, tag, protocol, port, a server-computed
|
||||||
tlsFlowCapable flag (true for VLESS on TCP with tls or reality, or on
|
tlsFlowCapable flag (true for VLESS on TCP with tls or reality, or on
|
||||||
XHTTP with VLESS encryption / vlessenc enabled), and ssMethod (the
|
XHTTP with VLESS encryption / vlessenc enabled), and ssMethod (the
|
||||||
@@ -38,110 +32,86 @@ _openapi:
|
|||||||
dropdowns and attach pickers — it skips settings, streamSettings, and
|
dropdowns and attach pickers — it skips settings, streamSettings, and
|
||||||
clientStats so the payload stays small even on panels with thousands of
|
clientStats so the payload stays small even on panels with thousands of
|
||||||
clients.
|
clients.
|
||||||
url: >-
|
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'
|
||||||
#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
|
- depth: 2
|
||||||
title: Fetch a single inbound by numeric ID.
|
title: Fetch a single inbound by numeric ID.
|
||||||
url: '#fetch-a-single-inbound-by-numeric-id'
|
url: '#fetch-a-single-inbound-by-numeric-id'
|
||||||
- depth: 2
|
- depth: 2
|
||||||
title: >-
|
title: Create a new inbound. Send the full inbound payload (protocol, port,
|
||||||
Create a new inbound. Send the full inbound payload (protocol, port,
|
|
||||||
settings, streamSettings, sniffing, remark, expiryTime, total, enable).
|
settings, streamSettings, sniffing, remark, expiryTime, total, enable).
|
||||||
settings, streamSettings, and sniffing may be sent as nested JSON
|
settings, streamSettings, and sniffing may be sent as nested JSON
|
||||||
objects (preferred) or as JSON-encoded strings (legacy).
|
objects (preferred) or as JSON-encoded strings (legacy).
|
||||||
url: >-
|
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'
|
||||||
#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
|
- depth: 2
|
||||||
title: Delete an inbound by ID. Also removes its associated client stats rows.
|
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'
|
url: '#delete-an-inbound-by-id-also-removes-its-associated-client-stats-rows'
|
||||||
- depth: 2
|
- depth: 2
|
||||||
title: >-
|
title: Delete many inbounds in one call. Processes the list sequentially;
|
||||||
Delete many inbounds in one call. Processes the list sequentially;
|
|
||||||
failures are reported per id and the rest still proceed. Restarts xray
|
failures are reported per id and the rest still proceed. Restarts xray
|
||||||
at most once.
|
at most once.
|
||||||
url: >-
|
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'
|
||||||
#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
|
- depth: 2
|
||||||
title: >-
|
title: Replace an inbound’s configuration. Body shape mirrors /add. Heavy on
|
||||||
Replace an inbound’s configuration. Body shape mirrors /add. Heavy on
|
|
||||||
inbounds with thousands of clients — prefer /setEnable for enable-only
|
inbounds with thousands of clients — prefer /setEnable for enable-only
|
||||||
flips.
|
flips.
|
||||||
url: >-
|
url: '#replace-an-inbounds-configuration-body-shape-mirrors-add-heavy-on-inbounds-with-thousands-of-clients--prefer-setenable-for-enable-only-flips'
|
||||||
#replace-an-inbounds-configuration-body-shape-mirrors-add-heavy-on-inbounds-with-thousands-of-clients--prefer-setenable-for-enable-only-flips
|
|
||||||
- depth: 2
|
- depth: 2
|
||||||
title: >-
|
title: Toggle only the enable flag without serialising the whole settings JSON.
|
||||||
Toggle only the enable flag without serialising the whole settings JSON.
|
|
||||||
Recommended for UI switches on large inbounds.
|
Recommended for UI switches on large inbounds.
|
||||||
url: >-
|
url: '#toggle-only-the-enable-flag-without-serialising-the-whole-settings-json-recommended-for-ui-switches-on-large-inbounds'
|
||||||
#toggle-only-the-enable-flag-without-serialising-the-whole-settings-json-recommended-for-ui-switches-on-large-inbounds
|
|
||||||
- depth: 2
|
- depth: 2
|
||||||
title: >-
|
title: Zero out upload + download counters for a single inbound. Does not touch
|
||||||
Zero out upload + download counters for a single inbound. Does not touch
|
|
||||||
per-client counters.
|
per-client counters.
|
||||||
url: >-
|
url: '#zero-out-upload--download-counters-for-a-single-inbound-does-not-touch-per-client-counters'
|
||||||
#zero-out-upload--download-counters-for-a-single-inbound-does-not-touch-per-client-counters
|
|
||||||
- depth: 2
|
- depth: 2
|
||||||
title: >-
|
title: Remove every client attached to a single inbound while keeping the
|
||||||
Remove every client attached to a single inbound while keeping the
|
|
||||||
inbound itself. Collects emails from settings.clients[] and feeds them
|
inbound itself. Collects emails from settings.clients[] and feeds them
|
||||||
into the optimized bulk-delete path (runtime user removal + traffic-row
|
into the optimized bulk-delete path (runtime user removal + traffic-row
|
||||||
cleanup + SyncInbound). Destructive and cannot be undone.
|
cleanup + SyncInbound). Destructive and cannot be undone.
|
||||||
url: >-
|
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'
|
||||||
#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
|
- depth: 2
|
||||||
title: >-
|
title: Reset upload + download counters on every inbound. Destructive —
|
||||||
Reset upload + download counters on every inbound. Destructive —
|
|
||||||
accounting history is lost.
|
accounting history is lost.
|
||||||
url: >-
|
url: '#reset-upload--download-counters-on-every-inbound-destructive--accounting-history-is-lost'
|
||||||
#reset-upload--download-counters-on-every-inbound-destructive--accounting-history-is-lost
|
|
||||||
- depth: 2
|
- depth: 2
|
||||||
title: >-
|
title: Bulk-import an inbound from a JSON blob (e.g. one exported via the UI).
|
||||||
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.
|
The body uses form encoding with a single "data" field.
|
||||||
url: >-
|
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'
|
||||||
#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
|
- depth: 2
|
||||||
title: >-
|
title: Receive a master panel's aggregated per-client usage, keyed by the
|
||||||
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
|
master's GUID. Stored in a side table used only for the UI display
|
||||||
overlay and local quota enforcement — never folded into the local
|
overlay and local quota enforcement — never folded into the local
|
||||||
counters that masters poll, so delta accounting stays intact. Called
|
counters that masters poll, so delta accounting stays intact. Called
|
||||||
panel-to-panel by the node traffic sync job.
|
panel-to-panel by the node traffic sync job.
|
||||||
url: >-
|
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'
|
||||||
#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
|
- depth: 2
|
||||||
title: >-
|
title: List the fallback rules attached to a master VLESS/Trojan TCP-TLS
|
||||||
List the fallback rules attached to a master VLESS/Trojan TCP-TLS
|
|
||||||
inbound. Each rule links one child inbound (the dest) to optional
|
inbound. Each rule links one child inbound (the dest) to optional
|
||||||
SNI/ALPN/path/dest/xver match criteria. When dest is empty the child
|
SNI/ALPN/path/dest/xver match criteria. When dest is empty the child
|
||||||
inbound's listen+port is used.
|
inbound's listen+port is used.
|
||||||
url: >-
|
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'
|
||||||
#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
|
- depth: 2
|
||||||
title: >-
|
title: Replace the entire fallback list for a master inbound. Body is JSON.
|
||||||
Replace the entire fallback list for a master inbound. Body is JSON.
|
|
||||||
Triggers an Xray restart.
|
Triggers an Xray restart.
|
||||||
url: >-
|
url: '#replace-the-entire-fallback-list-for-a-master-inbound-body-is-json-triggers-an-xray-restart'
|
||||||
#replace-the-entire-fallback-list-for-a-master-inbound-body-is-json-triggers-an-xray-restart
|
- depth: 2
|
||||||
|
title: Set only the subscription sort order. Reads the stored inbound, so a
|
||||||
|
reorder cannot carry a stale client list over a concurrent edit.
|
||||||
|
url: '#set-only-the-subscription-sort-order-reads-the-stored-inbound-so-a-reorder-cannot-carry-a-stale-client-list-over-a-concurrent-edit'
|
||||||
structuredData:
|
structuredData:
|
||||||
headings:
|
headings:
|
||||||
- content: >-
|
- content: List every inbound owned by the authenticated user, including each
|
||||||
List every inbound owned by the authenticated user, including each
|
|
||||||
inbound’s clientStats traffic counters. settings, streamSettings, and
|
inbound’s clientStats traffic counters. settings, streamSettings, and
|
||||||
sniffing are returned as nested JSON objects (no escaped strings);
|
sniffing are returned as nested JSON objects (no escaped strings);
|
||||||
legacy callers that send them back as JSON-encoded strings are still
|
legacy callers that send them back as JSON-encoded strings are still
|
||||||
accepted on write.
|
accepted on write.
|
||||||
id: >-
|
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
|
||||||
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
|
||||||
- content: >-
|
|
||||||
Same shape as /list but with settings.clients[] stripped down to
|
|
||||||
{email, enable, comment} and ClientStats not enriched with UUID/SubId.
|
{email, enable, comment} and ClientStats not enriched with UUID/SubId.
|
||||||
Use this for list pages; fetch /get/:id when you need the full
|
Use this for list pages; fetch /get/:id when you need the full
|
||||||
per-client payload (uuid, password, flow, ...).
|
per-client payload (uuid, password, flow, ...).
|
||||||
id: >-
|
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-
|
||||||
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.
|
||||||
- content: >-
|
|
||||||
Lightweight picker projection of the authenticated user’s inbounds.
|
|
||||||
Returns id, remark, tag, protocol, port, a server-computed
|
Returns id, remark, tag, protocol, port, a server-computed
|
||||||
tlsFlowCapable flag (true for VLESS on TCP with tls or reality, or on
|
tlsFlowCapable flag (true for VLESS on TCP with tls or reality, or on
|
||||||
XHTTP with VLESS encryption / vlessenc enabled), and ssMethod (the
|
XHTTP with VLESS encryption / vlessenc enabled), and ssMethod (the
|
||||||
@@ -150,80 +120,58 @@ _openapi:
|
|||||||
dropdowns and attach pickers — it skips settings, streamSettings, and
|
dropdowns and attach pickers — it skips settings, streamSettings, and
|
||||||
clientStats so the payload stays small even on panels with thousands
|
clientStats so the payload stays small even on panels with thousands
|
||||||
of clients.
|
of clients.
|
||||||
id: >-
|
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
|
||||||
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: Fetch a single inbound by numeric ID.
|
- content: Fetch a single inbound by numeric ID.
|
||||||
id: fetch-a-single-inbound-by-numeric-id
|
id: fetch-a-single-inbound-by-numeric-id
|
||||||
- content: >-
|
- content: Create a new inbound. Send the full inbound payload (protocol, port,
|
||||||
Create a new inbound. Send the full inbound payload (protocol, port,
|
|
||||||
settings, streamSettings, sniffing, remark, expiryTime, total,
|
settings, streamSettings, sniffing, remark, expiryTime, total,
|
||||||
enable). settings, streamSettings, and sniffing may be sent as nested
|
enable). settings, streamSettings, and sniffing may be sent as nested
|
||||||
JSON objects (preferred) or as JSON-encoded strings (legacy).
|
JSON objects (preferred) or as JSON-encoded strings (legacy).
|
||||||
id: >-
|
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
|
||||||
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.
|
||||||
- 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
|
id: delete-an-inbound-by-id-also-removes-its-associated-client-stats-rows
|
||||||
- content: >-
|
- content: Delete many inbounds in one call. Processes the list sequentially;
|
||||||
Delete many inbounds in one call. Processes the list sequentially;
|
|
||||||
failures are reported per id and the rest still proceed. Restarts xray
|
failures are reported per id and the rest still proceed. Restarts xray
|
||||||
at most once.
|
at most once.
|
||||||
id: >-
|
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
|
||||||
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
|
||||||
- content: >-
|
|
||||||
Replace an inbound’s configuration. Body shape mirrors /add. Heavy on
|
|
||||||
inbounds with thousands of clients — prefer /setEnable for enable-only
|
inbounds with thousands of clients — prefer /setEnable for enable-only
|
||||||
flips.
|
flips.
|
||||||
id: >-
|
id: replace-an-inbounds-configuration-body-shape-mirrors-add-heavy-on-inbounds-with-thousands-of-clients--prefer-setenable-for-enable-only-flips
|
||||||
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
|
||||||
- content: >-
|
|
||||||
Toggle only the enable flag without serialising the whole settings
|
|
||||||
JSON. Recommended for UI switches on large inbounds.
|
JSON. Recommended for UI switches on large inbounds.
|
||||||
id: >-
|
id: toggle-only-the-enable-flag-without-serialising-the-whole-settings-json-recommended-for-ui-switches-on-large-inbounds
|
||||||
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
|
||||||
- content: >-
|
|
||||||
Zero out upload + download counters for a single inbound. Does not
|
|
||||||
touch per-client counters.
|
touch per-client counters.
|
||||||
id: >-
|
id: zero-out-upload--download-counters-for-a-single-inbound-does-not-touch-per-client-counters
|
||||||
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
|
||||||
- content: >-
|
|
||||||
Remove every client attached to a single inbound while keeping the
|
|
||||||
inbound itself. Collects emails from settings.clients[] and feeds them
|
inbound itself. Collects emails from settings.clients[] and feeds them
|
||||||
into the optimized bulk-delete path (runtime user removal +
|
into the optimized bulk-delete path (runtime user removal +
|
||||||
traffic-row cleanup + SyncInbound). Destructive and cannot be undone.
|
traffic-row cleanup + SyncInbound). Destructive and cannot be undone.
|
||||||
id: >-
|
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
|
||||||
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 —
|
||||||
- content: >-
|
|
||||||
Reset upload + download counters on every inbound. Destructive —
|
|
||||||
accounting history is lost.
|
accounting history is lost.
|
||||||
id: >-
|
id: reset-upload--download-counters-on-every-inbound-destructive--accounting-history-is-lost
|
||||||
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).
|
||||||
- content: >-
|
The body uses form encoding with a single "data" field.
|
||||||
Bulk-import an inbound from a JSON blob (e.g. one exported via the
|
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
|
||||||
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: >-
|
|
||||||
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
|
master's GUID. Stored in a side table used only for the UI display
|
||||||
overlay and local quota enforcement — never folded into the local
|
overlay and local quota enforcement — never folded into the local
|
||||||
counters that masters poll, so delta accounting stays intact. Called
|
counters that masters poll, so delta accounting stays intact. Called
|
||||||
panel-to-panel by the node traffic sync job.
|
panel-to-panel by the node traffic sync job.
|
||||||
id: >-
|
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
|
||||||
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
|
||||||
- content: >-
|
|
||||||
List the fallback rules attached to a master VLESS/Trojan TCP-TLS
|
|
||||||
inbound. Each rule links one child inbound (the dest) to optional
|
inbound. Each rule links one child inbound (the dest) to optional
|
||||||
SNI/ALPN/path/dest/xver match criteria. When dest is empty the child
|
SNI/ALPN/path/dest/xver match criteria. When dest is empty the child
|
||||||
inbound's listen+port is used.
|
inbound's listen+port is used.
|
||||||
id: >-
|
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
|
||||||
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.
|
||||||
- content: >-
|
|
||||||
Replace the entire fallback list for a master inbound. Body is JSON.
|
|
||||||
Triggers an Xray restart.
|
Triggers an Xray restart.
|
||||||
id: >-
|
id: replace-the-entire-fallback-list-for-a-master-inbound-body-is-json-triggers-an-xray-restart
|
||||||
replace-the-entire-fallback-list-for-a-master-inbound-body-is-json-triggers-an-xray-restart
|
- content: Set only the subscription sort order. Reads the stored inbound, so a
|
||||||
|
reorder cannot carry a stale client list over a concurrent edit.
|
||||||
|
id: set-only-the-subscription-sort-order-reads-the-stored-inbound-so-a-reorder-cannot-carry-a-stale-client-list-over-a-concurrent-edit
|
||||||
contents: []
|
contents: []
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -236,7 +184,7 @@ export default function Layout(props) {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{props.children}
|
{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/get/{id}","method":"get"},{"path":"/panel/api/inbounds/add","method":"post"},{"path":"/panel/api/inbounds/del/{id}","method":"post"},{"path":"/panel/api/inbounds/bulkDel","method":"post"},{"path":"/panel/api/inbounds/update/{id}","method":"post"},{"path":"/panel/api/inbounds/setEnable/{id}","method":"post"},{"path":"/panel/api/inbounds/{id}/resetTraffic","method":"post"},{"path":"/panel/api/inbounds/{id}/delAllClients","method":"post"},{"path":"/panel/api/inbounds/resetAllTraffics","method":"post"},{"path":"/panel/api/inbounds/import","method":"post"},{"path":"/panel/api/inbounds/pushClientTraffics","method":"post"},{"path":"/panel/api/inbounds/{id}/fallbacks","method":"get"},{"path":"/panel/api/inbounds/{id}/fallbacks","method":"post"},{"path":"/panel/api/inbounds/{id}/subSortIndex","method":"post"}]} showTitle />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1,51 +1,40 @@
|
|||||||
---
|
---
|
||||||
title: Nodes
|
title: Nodes
|
||||||
description: >-
|
description: Manage remote 3x-ui panels acting as nodes for a central panel. All
|
||||||
Manage remote 3x-ui panels acting as nodes for a central panel. All endpoints
|
endpoints under /panel/api/nodes.
|
||||||
under /panel/api/nodes.
|
|
||||||
full: true
|
full: true
|
||||||
_openapi:
|
_openapi:
|
||||||
preload:
|
preload:
|
||||||
- ./public/openapi.json
|
- ./public/openapi.json
|
||||||
toc:
|
toc:
|
||||||
- depth: 2
|
- depth: 2
|
||||||
title: >-
|
title: List every configured node with its connection details, health, and last
|
||||||
List every configured node with its connection details, health, and last
|
|
||||||
heartbeat patch.
|
heartbeat patch.
|
||||||
url: >-
|
url: '#list-every-configured-node-with-its-connection-details-health-and-last-heartbeat-patch'
|
||||||
#list-every-configured-node-with-its-connection-details-health-and-last-heartbeat-patch
|
|
||||||
- depth: 2
|
- depth: 2
|
||||||
title: >-
|
title: This panel's node-auth CA certificate (public, PEM) to paste into a
|
||||||
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
|
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.
|
cert on first call. Pair with setting tlsVerifyMode=mtls on the node.
|
||||||
url: >-
|
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'
|
||||||
#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
|
- depth: 2
|
||||||
title: >-
|
title: Set the CA certificate this panel trusts for incoming node-API client
|
||||||
Set the CA certificate this panel trusts for incoming node-API client
|
|
||||||
certificates (this panel acting as a node). Paste the managing panel's
|
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
|
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.
|
must be a PEM certificate. Applied on the next panel restart.
|
||||||
url: >-
|
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'
|
||||||
#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
|
- depth: 2
|
||||||
title: Fetch a single node by ID.
|
title: Fetch a single node by ID.
|
||||||
url: '#fetch-a-single-node-by-id'
|
url: '#fetch-a-single-node-by-id'
|
||||||
- depth: 2
|
- depth: 2
|
||||||
title: >-
|
title: Fetch a node's own web TLS certificate/key file paths (proxied to the
|
||||||
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). 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
|
node-assigned inbound gets paths that exist on the node, not the central
|
||||||
panel.
|
panel.
|
||||||
url: >-
|
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'
|
||||||
#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
|
- depth: 2
|
||||||
title: >-
|
title: Register a new remote node. Provide its URL, apiToken, and optional
|
||||||
Register a new remote node. Provide its URL, apiToken, and optional
|
|
||||||
remark / allowPrivateAddress flag.
|
remark / allowPrivateAddress flag.
|
||||||
url: >-
|
url: '#register-a-new-remote-node-provide-its-url-apitoken-and-optional-remark--allowprivateaddress-flag'
|
||||||
#register-a-new-remote-node-provide-its-url-apitoken-and-optional-remark--allowprivateaddress-flag
|
|
||||||
- depth: 2
|
- depth: 2
|
||||||
title: Replace a node’s connection details. Same body shape as /add.
|
title: Replace a node’s connection details. Same body shape as /add.
|
||||||
url: '#replace-a-nodes-connection-details-same-body-shape-as-add'
|
url: '#replace-a-nodes-connection-details-same-body-shape-as-add'
|
||||||
@@ -56,115 +45,94 @@ _openapi:
|
|||||||
title: Pause or resume traffic sync with this node.
|
title: Pause or resume traffic sync with this node.
|
||||||
url: '#pause-or-resume-traffic-sync-with-this-node'
|
url: '#pause-or-resume-traffic-sync-with-this-node'
|
||||||
- depth: 2
|
- depth: 2
|
||||||
title: >-
|
title: Probe a node without saving it. Uses the body as connection details and
|
||||||
Probe a node without saving it. Uses the body as connection details and
|
|
||||||
returns the same heartbeat snapshot a registered node would have.
|
returns the same heartbeat snapshot a registered node would have.
|
||||||
url: >-
|
url: '#probe-a-node-without-saving-it-uses-the-body-as-connection-details-and-returns-the-same-heartbeat-snapshot-a-registered-node-would-have'
|
||||||
#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
|
- depth: 2
|
||||||
title: >-
|
title: Connect to the node over HTTPS without verifying its certificate and
|
||||||
Connect to the node over HTTPS without verifying its certificate and
|
|
||||||
return the leaf certificate's SHA-256 (base64). Used by the Add/Edit
|
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
|
Node dialog to fetch and pin a self-signed certificate. Uses the same
|
||||||
body as /test.
|
body as /test.
|
||||||
url: >-
|
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'
|
||||||
#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
|
- depth: 2
|
||||||
title: >-
|
title: Use unsaved node connection details to list the remote inbounds available
|
||||||
Use unsaved node connection details to list the remote inbounds
|
for selective import.
|
||||||
available for selective import.
|
url: '#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
|
- depth: 2
|
||||||
title: Probe an existing node, updating its cached health state.
|
title: Probe an existing node, updating its cached health state.
|
||||||
url: '#probe-an-existing-node-updating-its-cached-health-state'
|
url: '#probe-an-existing-node-updating-its-cached-health-state'
|
||||||
- depth: 2
|
- depth: 2
|
||||||
title: >-
|
title: 'Trigger the official panel self-updater on each given node (downloads
|
||||||
Trigger the official panel self-updater on each given node (downloads
|
|
||||||
the latest release and restarts). Only enabled, online nodes are
|
the latest release and restarts). Only enabled, online nodes are
|
||||||
updated; offline/disabled ones are reported as skipped. Set "dev": true
|
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
|
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: >-
|
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'
|
||||||
#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
|
- depth: 2
|
||||||
title: >-
|
title: Aggregated metric history for a node — same shape as /server/history,
|
||||||
Aggregated metric history for a node — same shape as /server/history,
|
|
||||||
scoped to one node.
|
scoped to one node.
|
||||||
url: >-
|
url: '#aggregated-metric-history-for-a-node--same-shape-as-serverhistory-scoped-to-one-node'
|
||||||
#aggregated-metric-history-for-a-node--same-shape-as-serverhistory-scoped-to-one-node
|
- depth: 2
|
||||||
|
title: Validate the stored master mTLS client credential and invalidate cached
|
||||||
|
transports. Each transport closes its old idle pool and rebuilds with
|
||||||
|
the rotated certificate before its next request.
|
||||||
|
url: '#validate-the-stored-master-mtls-client-credential-and-invalidate-cached-transports-each-transport-closes-its-old-idle-pool-and-rebuilds-with-the-rotated-certificate-before-its-next-request'
|
||||||
structuredData:
|
structuredData:
|
||||||
headings:
|
headings:
|
||||||
- content: >-
|
- content: List every configured node with its connection details, health, and
|
||||||
List every configured node with its connection details, health, and
|
|
||||||
last heartbeat patch.
|
last heartbeat patch.
|
||||||
id: >-
|
id: list-every-configured-node-with-its-connection-details-health-and-last-heartbeat-patch
|
||||||
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
|
||||||
- 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
|
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.
|
cert on first call. Pair with setting tlsVerifyMode=mtls on the node.
|
||||||
id: >-
|
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
|
||||||
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
|
||||||
- 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
|
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
|
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.
|
value must be a PEM certificate. Applied on the next panel restart.
|
||||||
id: >-
|
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
|
||||||
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: Fetch a single node by ID.
|
- content: Fetch a single node by ID.
|
||||||
id: fetch-a-single-node-by-id
|
id: fetch-a-single-node-by-id
|
||||||
- content: >-
|
- content: Fetch a node's own web TLS certificate/key file paths (proxied to the
|
||||||
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). Used by the inbound form's "Set Cert from Panel" so a
|
||||||
node-assigned inbound gets paths that exist on the node, not the
|
node-assigned inbound gets paths that exist on the node, not the
|
||||||
central panel.
|
central panel.
|
||||||
id: >-
|
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
|
||||||
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
|
||||||
- content: >-
|
|
||||||
Register a new remote node. Provide its URL, apiToken, and optional
|
|
||||||
remark / allowPrivateAddress flag.
|
remark / allowPrivateAddress flag.
|
||||||
id: >-
|
id: register-a-new-remote-node-provide-its-url-apitoken-and-optional-remark--allowprivateaddress-flag
|
||||||
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.
|
- content: Replace a node’s connection details. Same body shape as /add.
|
||||||
id: replace-a-nodes-connection-details-same-body-shape-as-add
|
id: replace-a-nodes-connection-details-same-body-shape-as-add
|
||||||
- content: Delete a node. Inbounds bound to it are not auto-migrated.
|
- content: Delete a node. Inbounds bound to it are not auto-migrated.
|
||||||
id: 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.
|
- content: Pause or resume traffic sync with this node.
|
||||||
id: pause-or-resume-traffic-sync-with-this-node
|
id: pause-or-resume-traffic-sync-with-this-node
|
||||||
- content: >-
|
- content: Probe a node without saving it. Uses the body as connection details and
|
||||||
Probe a node without saving it. Uses the body as connection details
|
returns the same heartbeat snapshot a registered node would have.
|
||||||
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
|
||||||
id: >-
|
- content: Connect to the node over HTTPS without verifying its certificate and
|
||||||
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
|
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
|
Node dialog to fetch and pin a self-signed certificate. Uses the same
|
||||||
body as /test.
|
body as /test.
|
||||||
id: >-
|
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
|
||||||
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
|
||||||
- content: >-
|
|
||||||
Use unsaved node connection details to list the remote inbounds
|
|
||||||
available for selective import.
|
available for selective import.
|
||||||
id: >-
|
id: use-unsaved-node-connection-details-to-list-the-remote-inbounds-available-for-selective-import
|
||||||
use-unsaved-node-connection-details-to-list-the-remote-inbounds-available-for-selective-import
|
|
||||||
- content: Probe an existing node, updating its cached health state.
|
- content: Probe an existing node, updating its cached health state.
|
||||||
id: probe-an-existing-node-updating-its-cached-health-state
|
id: probe-an-existing-node-updating-its-cached-health-state
|
||||||
- content: >-
|
- content: 'Trigger the official panel self-updater on each given node (downloads
|
||||||
Trigger the official panel self-updater on each given node (downloads
|
|
||||||
the latest release and restarts). Only enabled, online nodes are
|
the latest release and restarts). Only enabled, online nodes are
|
||||||
updated; offline/disabled ones are reported as skipped. Set "dev":
|
updated; offline/disabled ones are reported as skipped. Set "dev":
|
||||||
true to move the nodes to the rolling per-commit dev channel instead
|
true to move the nodes to the rolling per-commit dev channel instead
|
||||||
of the latest stable release. Returns a per-node result list.
|
of the latest stable release. Returns a per-node result list.'
|
||||||
id: >-
|
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
|
||||||
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,
|
||||||
- content: >-
|
|
||||||
Aggregated metric history for a node — same shape as /server/history,
|
|
||||||
scoped to one node.
|
scoped to one node.
|
||||||
id: >-
|
id: aggregated-metric-history-for-a-node--same-shape-as-serverhistory-scoped-to-one-node
|
||||||
aggregated-metric-history-for-a-node--same-shape-as-serverhistory-scoped-to-one-node
|
- content: Validate the stored master mTLS client credential and invalidate cached
|
||||||
|
transports. Each transport closes its old idle pool and rebuilds with
|
||||||
|
the rotated certificate before its next request.
|
||||||
|
id: validate-the-stored-master-mtls-client-credential-and-invalidate-cached-transports-each-transport-closes-its-old-idle-pool-and-rebuilds-with-the-rotated-certificate-before-its-next-request
|
||||||
contents: []
|
contents: []
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -177,7 +145,7 @@ export default function Layout(props) {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{props.children}
|
{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/get/{id}","method":"get"},{"path":"/panel/api/nodes/webCert/{id}","method":"get"},{"path":"/panel/api/nodes/add","method":"post"},{"path":"/panel/api/nodes/update/{id}","method":"post"},{"path":"/panel/api/nodes/del/{id}","method":"post"},{"path":"/panel/api/nodes/setEnable/{id}","method":"post"},{"path":"/panel/api/nodes/test","method":"post"},{"path":"/panel/api/nodes/certFingerprint","method":"post"},{"path":"/panel/api/nodes/inbounds","method":"post"},{"path":"/panel/api/nodes/probe/{id}","method":"post"},{"path":"/panel/api/nodes/updatePanel","method":"post"},{"path":"/panel/api/nodes/history/{id}/{metric}/{bucket}","method":"get"},{"path":"/panel/api/nodes/mtls/reloadClient","method":"post"}]} showTitle />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1,65 +1,48 @@
|
|||||||
---
|
---
|
||||||
title: Server
|
title: Server
|
||||||
description: >-
|
description: System status, log retrieval, certificate generators, Xray binary
|
||||||
System status, log retrieval, certificate generators, Xray binary management,
|
management, and backup/restore. All under /panel/api/server.
|
||||||
and backup/restore. All under /panel/api/server.
|
|
||||||
full: true
|
full: true
|
||||||
_openapi:
|
_openapi:
|
||||||
preload:
|
preload:
|
||||||
- ./public/openapi.json
|
- ./public/openapi.json
|
||||||
toc:
|
toc:
|
||||||
- depth: 2
|
- depth: 2
|
||||||
title: >-
|
title: 'Real-time machine snapshot: CPU, memory, swap, disk, network IO, load
|
||||||
Real-time machine snapshot: CPU, memory, swap, disk, network IO, load
|
|
||||||
averages, open connections, Xray state. Cached and refreshed every 2
|
averages, open connections, Xray state. Cached and refreshed every 2
|
||||||
seconds in the background.
|
seconds in the background.'
|
||||||
url: >-
|
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'
|
||||||
#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
|
- depth: 2
|
||||||
title: >-
|
title: Reports whether per-client IP limits can be enforced on this host. The
|
||||||
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
|
panel uses it to gate the "IP Limit" field, since enforcement depends on
|
||||||
Fail2ban being installed.
|
Fail2ban being installed.
|
||||||
url: >-
|
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'
|
||||||
#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
|
- depth: 2
|
||||||
title: >-
|
title: 'Legacy: aggregated CPU history. Use /history/cpu/:bucket instead — same
|
||||||
Legacy: aggregated CPU history. Use /history/cpu/:bucket instead — same
|
data with a uniform {t, v} shape.'
|
||||||
data with a uniform {t, v} shape.
|
url: '#legacy-aggregated-cpu-history-use-historycpubucket-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
|
- depth: 2
|
||||||
title: >-
|
title: Aggregated time-series for one metric. Returns an array of {t, v} samples
|
||||||
Aggregated time-series for one metric. Returns an array of {t, v}
|
covering the last ~6 hours.
|
||||||
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'
|
||||||
url: >-
|
|
||||||
#aggregated-time-series-for-one-metric-returns-an-array-of-t-v-samples-covering-the-last-6-hours
|
|
||||||
- depth: 2
|
- depth: 2
|
||||||
title: >-
|
title: Xray runtime metrics state — whether the xray config has a `metrics`
|
||||||
Xray runtime metrics state — whether the xray config has a `metrics`
|
|
||||||
block, which expvar keys are flowing, and the current snapshot values
|
block, which expvar keys are flowing, and the current snapshot values
|
||||||
for each. Returns an empty state when metrics are not configured.
|
for each. Returns an empty state when metrics are not configured.
|
||||||
url: >-
|
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'
|
||||||
#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
|
- depth: 2
|
||||||
title: >-
|
title: Time-series history for one Xray runtime metric over the last ~6 hours.
|
||||||
Time-series history for one Xray runtime metric over the last ~6 hours.
|
|
||||||
Same {t, v} shape as /history/:metric/:bucket.
|
Same {t, v} shape as /history/:metric/:bucket.
|
||||||
url: >-
|
url: '#time-series-history-for-one-xray-runtime-metric-over-the-last-6-hours-same-t-v-shape-as-historymetricbucket'
|
||||||
#time-series-history-for-one-xray-runtime-metric-over-the-last-6-hours-same-t-v-shape-as-historymetricbucket
|
|
||||||
- depth: 2
|
- depth: 2
|
||||||
title: >-
|
title: Latest snapshot from the Xray observatory — per-outbound latency, health
|
||||||
Latest snapshot from the Xray observatory — per-outbound latency, health
|
|
||||||
status, and last-probe time. Only populated when the Xray config has an
|
status, and last-probe time. Only populated when the Xray config has an
|
||||||
observatory configured.
|
observatory configured.
|
||||||
url: >-
|
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'
|
||||||
#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
|
- depth: 2
|
||||||
title: >-
|
title: Time-series of observatory probe results for one outbound tag. Same {t,
|
||||||
Time-series of observatory probe results for one outbound tag. Same {t,
|
|
||||||
v} shape as the other history endpoints.
|
v} shape as the other history endpoints.
|
||||||
url: >-
|
url: '#time-series-of-observatory-probe-results-for-one-outbound-tag-same-t-v-shape-as-the-other-history-endpoints'
|
||||||
#time-series-of-observatory-probe-results-for-one-outbound-tag-same-t-v-shape-as-the-other-history-endpoints
|
|
||||||
- depth: 2
|
- depth: 2
|
||||||
title: List Xray binary versions available for install on this host.
|
title: List Xray binary versions available for install on this host.
|
||||||
url: '#list-xray-binary-versions-available-for-install-on-this-host'
|
url: '#list-xray-binary-versions-available-for-install-on-this-host'
|
||||||
@@ -70,90 +53,66 @@ _openapi:
|
|||||||
title: Return the assembled Xray config that’s currently running on this host.
|
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'
|
url: '#return-the-assembled-xray-config-thats-currently-running-on-this-host'
|
||||||
- depth: 2
|
- depth: 2
|
||||||
title: >-
|
title: Stream the SQLite database file as an attachment. Use as a manual backup.
|
||||||
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'
|
url: '#stream-the-sqlite-database-file-as-an-attachment-use-as-a-manual-backup'
|
||||||
- depth: 2
|
- depth: 2
|
||||||
title: >-
|
title: 'Stream a cross-engine migration file as an attachment: a .dump (SQL
|
||||||
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
|
text) on SQLite, or a .db SQLite database built from the live data on
|
||||||
PostgreSQL.
|
PostgreSQL.'
|
||||||
url: >-
|
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'
|
||||||
#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
|
- depth: 2
|
||||||
title: Generate a fresh UUID v4. Convenience helper for client IDs.
|
title: Generate a fresh UUID v4. Convenience helper for client IDs.
|
||||||
url: '#generate-a-fresh-uuid-v4-convenience-helper-for-client-ids'
|
url: '#generate-a-fresh-uuid-v4-convenience-helper-for-client-ids'
|
||||||
- depth: 2
|
- depth: 2
|
||||||
title: >-
|
title: Return this panel's own web TLS certificate and key file paths. The
|
||||||
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
|
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
|
from Panel" fills a node-assigned inbound with paths that exist on the
|
||||||
node.
|
node.
|
||||||
url: >-
|
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'
|
||||||
#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
|
- depth: 2
|
||||||
title: >-
|
title: Read-only summaries (guid, parentGuid, name, address, status, versions)
|
||||||
Read-only summaries (guid, parentGuid, name, address, status, versions)
|
|
||||||
of the nodes this panel manages. A parent panel calls it on a node (via
|
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
|
the node API token) to surface transitive sub-nodes in a chained
|
||||||
topology. Counts are computed by the parent, not returned here.
|
topology. Counts are computed by the parent, not returned here.
|
||||||
url: >-
|
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'
|
||||||
#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
|
- depth: 2
|
||||||
title: Generate a new X25519 keypair for Reality.
|
title: Generate a new X25519 keypair for Reality.
|
||||||
url: '#generate-a-new-x25519-keypair-for-reality'
|
url: '#generate-a-new-x25519-keypair-for-reality'
|
||||||
- depth: 2
|
- depth: 2
|
||||||
title: >-
|
title: Generate a new ML-DSA-65 keypair (post-quantum signature). Returns
|
||||||
Generate a new ML-DSA-65 keypair (post-quantum signature). Returns
|
|
||||||
{privateKey, publicKey, seed}.
|
{privateKey, publicKey, seed}.
|
||||||
url: >-
|
url: '#generate-a-new-ml-dsa-65-keypair-post-quantum-signature-returns-privatekey-publickey-seed'
|
||||||
#generate-a-new-ml-dsa-65-keypair-post-quantum-signature-returns-privatekey-publickey-seed
|
|
||||||
- depth: 2
|
- depth: 2
|
||||||
title: >-
|
title: Generate a new ML-KEM-768 keypair (post-quantum KEM). Returns {clientKey,
|
||||||
Generate a new ML-KEM-768 keypair (post-quantum KEM). Returns
|
serverKey}.
|
||||||
{clientKey, serverKey}.
|
url: '#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
|
- depth: 2
|
||||||
title: >-
|
title: Generate VLESS encryption auth options. Returns an auths array each with
|
||||||
Generate VLESS encryption auth options. Returns an auths array each with
|
|
||||||
id, label, encryption, and decryption fields.
|
id, label, encryption, and decryption fields.
|
||||||
url: >-
|
url: '#generate-vless-encryption-auth-options-returns-an-auths-array-each-with-id-label-encryption-and-decryption-fields'
|
||||||
#generate-vless-encryption-auth-options-returns-an-auths-array-each-with-id-label-encryption-and-decryption-fields
|
|
||||||
- depth: 2
|
- depth: 2
|
||||||
title: Stop the Xray binary. All proxies go offline immediately.
|
title: Stop the Xray binary. All proxies go offline immediately.
|
||||||
url: '#stop-the-xray-binary-all-proxies-go-offline-immediately'
|
url: '#stop-the-xray-binary-all-proxies-go-offline-immediately'
|
||||||
- depth: 2
|
- depth: 2
|
||||||
title: >-
|
title: Reload Xray with the current config. Typically required after structural
|
||||||
Reload Xray with the current config. Typically required after structural
|
|
||||||
inbound or routing changes.
|
inbound or routing changes.
|
||||||
url: >-
|
url: '#reload-xray-with-the-current-config-typically-required-after-structural-inbound-or-routing-changes'
|
||||||
#reload-xray-with-the-current-config-typically-required-after-structural-inbound-or-routing-changes
|
|
||||||
- depth: 2
|
- depth: 2
|
||||||
title: >-
|
title: Download and install the specified Xray version. Pass "latest" for the
|
||||||
Download and install the specified Xray version. Pass "latest" for the
|
|
||||||
newest release.
|
newest release.
|
||||||
url: >-
|
url: '#download-and-install-the-specified-xray-version-pass-latest-for-the-newest-release'
|
||||||
#download-and-install-the-specified-xray-version-pass-latest-for-the-newest-release
|
|
||||||
- depth: 2
|
- depth: 2
|
||||||
title: >-
|
title: Self-update the panel to the latest version. The server restarts on
|
||||||
Self-update the panel to the latest version. The server restarts on
|
|
||||||
success.
|
success.
|
||||||
url: >-
|
url: '#self-update-the-panel-to-the-latest-version-the-server-restarts-on-success'
|
||||||
#self-update-the-panel-to-the-latest-version-the-server-restarts-on-success
|
|
||||||
- depth: 2
|
- depth: 2
|
||||||
title: >-
|
title: Toggle the panel update channel between stable and the rolling per-commit
|
||||||
Toggle the panel update channel between stable and the rolling
|
dev release. Only effective on dev builds.
|
||||||
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'
|
||||||
url: >-
|
|
||||||
#toggle-the-panel-update-channel-between-stable-and-the-rolling-per-commit-dev-release-only-effective-on-dev-builds
|
|
||||||
- depth: 2
|
- depth: 2
|
||||||
title: >-
|
title: Refresh the default GeoIP / GeoSite data files. Body can include a
|
||||||
Refresh the default GeoIP / GeoSite data files. Body can include a
|
|
||||||
fileName, or use the /:fileName variant.
|
fileName, or use the /:fileName variant.
|
||||||
url: >-
|
url: '#refresh-the-default-geoip--geosite-data-files-body-can-include-a-filename-or-use-the-filename-variant'
|
||||||
#refresh-the-default-geoip--geosite-data-files-body-can-include-a-filename-or-use-the-filename-variant
|
|
||||||
- depth: 2
|
- depth: 2
|
||||||
title: Refresh a single Geo file by filename (e.g. geoip.dat, geosite.dat).
|
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'
|
url: '#refresh-a-single-geo-file-by-filename-eg-geoipdat-geositedat'
|
||||||
@@ -164,205 +123,138 @@ _openapi:
|
|||||||
title: Return the last N lines of the Xray process log.
|
title: Return the last N lines of the Xray process log.
|
||||||
url: '#return-the-last-n-lines-of-the-xray-process-log'
|
url: '#return-the-last-n-lines-of-the-xray-process-log'
|
||||||
- depth: 2
|
- depth: 2
|
||||||
title: >-
|
title: Restore the panel DB from an uploaded SQLite file (multipart form, field
|
||||||
Restore the panel DB from an uploaded SQLite file (multipart form, field
|
|
||||||
name "db"). The panel restarts after restore. Destructive.
|
name "db"). The panel restarts after restore. Destructive.
|
||||||
url: >-
|
url: '#restore-the-panel-db-from-an-uploaded-sqlite-file-multipart-form-field-name-db-the-panel-restarts-after-restore-destructive'
|
||||||
#restore-the-panel-db-from-an-uploaded-sqlite-file-multipart-form-field-name-db-the-panel-restarts-after-restore-destructive
|
|
||||||
- depth: 2
|
- depth: 2
|
||||||
title: >-
|
title: Generate a new ECH (Encrypted Client Hello) keypair and config list for
|
||||||
Generate a new ECH (Encrypted Client Hello) keypair and config list for
|
|
||||||
the given SNI.
|
the given SNI.
|
||||||
url: >-
|
url: '#generate-a-new-ech-encrypted-client-hello-keypair-and-config-list-for-the-given-sni'
|
||||||
#generate-a-new-ech-encrypted-client-hello-keypair-and-config-list-for-the-given-sni
|
|
||||||
- depth: 2
|
- depth: 2
|
||||||
title: >-
|
title: Compute the hex SHA-256 of a certificate (DER) for pinning
|
||||||
Compute the hex SHA-256 of a certificate (DER) for pinning
|
|
||||||
(pinnedPeerCertSha256). Provide either a server file path or inline
|
(pinnedPeerCertSha256). Provide either a server file path or inline
|
||||||
PEM/DER content.
|
PEM/DER content.
|
||||||
url: >-
|
url: '#compute-the-hex-sha-256-of-a-certificate-der-for-pinning-pinnedpeercertsha256-provide-either-a-server-file-path-or-inline-pemder-content'
|
||||||
#compute-the-hex-sha-256-of-a-certificate-der-for-pinning-pinnedpeercertsha256-provide-either-a-server-file-path-or-inline-pemder-content
|
|
||||||
- depth: 2
|
- depth: 2
|
||||||
title: >-
|
title: Run `xray tls ping` against a remote server and return its live
|
||||||
Run `xray tls ping` against a remote server and return its live
|
|
||||||
leaf-certificate SHA-256 hash(es) for pinning (pinnedPeerCertSha256).
|
leaf-certificate SHA-256 hash(es) for pinning (pinnedPeerCertSha256).
|
||||||
url: >-
|
url: '#run-xray-tls-ping-against-a-remote-server-and-return-its-live-leaf-certificate-sha-256-hashes-for-pinning-pinnedpeercertsha256'
|
||||||
#run-xray-tls-ping-against-a-remote-server-and-return-its-live-leaf-certificate-sha-256-hashes-for-pinning-pinnedpeercertsha256
|
|
||||||
- depth: 2
|
- depth: 2
|
||||||
title: >-
|
title: Fetch the fully aggregated inbound_client_ips database table. Used by
|
||||||
Fetch the fully aggregated inbound_client_ips database table. Used by
|
|
||||||
nodes to sync recently active IPs across the cluster.
|
nodes to sync recently active IPs across the cluster.
|
||||||
url: >-
|
url: '#fetch-the-fully-aggregated-inbound_client_ips-database-table-used-by-nodes-to-sync-recently-active-ips-across-the-cluster'
|
||||||
#fetch-the-fully-aggregated-inbound_client_ips-database-table-used-by-nodes-to-sync-recently-active-ips-across-the-cluster
|
|
||||||
- depth: 2
|
- depth: 2
|
||||||
title: >-
|
title: Submit a list of recently active IP timestamps. The panel merges them
|
||||||
Submit a list of recently active IP timestamps. The panel merges them
|
|
||||||
with the existing database to maintain a unified global IP-limit view.
|
with the existing database to maintain a unified global IP-limit view.
|
||||||
url: >-
|
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'
|
||||||
#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:
|
structuredData:
|
||||||
headings:
|
headings:
|
||||||
- content: >-
|
- content: 'Real-time machine snapshot: CPU, memory, swap, disk, network IO, load
|
||||||
Real-time machine snapshot: CPU, memory, swap, disk, network IO, load
|
|
||||||
averages, open connections, Xray state. Cached and refreshed every 2
|
averages, open connections, Xray state. Cached and refreshed every 2
|
||||||
seconds in the background.
|
seconds in the background.'
|
||||||
id: >-
|
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
|
||||||
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
|
||||||
- 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
|
panel uses it to gate the "IP Limit" field, since enforcement depends
|
||||||
on Fail2ban being installed.
|
on Fail2ban being installed.
|
||||||
id: >-
|
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
|
||||||
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 —
|
||||||
- content: >-
|
same data with a uniform {t, v} shape.'
|
||||||
Legacy: aggregated CPU history. Use /history/cpu/:bucket instead —
|
id: legacy-aggregated-cpu-history-use-historycpubucket-instead--same-data-with-a-uniform-t-v-shape
|
||||||
same data with a uniform {t, v} shape.
|
- content: Aggregated time-series for one metric. Returns an array of {t, v}
|
||||||
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.
|
samples covering the last ~6 hours.
|
||||||
id: >-
|
id: aggregated-time-series-for-one-metric-returns-an-array-of-t-v-samples-covering-the-last-6-hours
|
||||||
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`
|
||||||
- content: >-
|
|
||||||
Xray runtime metrics state — whether the xray config has a `metrics`
|
|
||||||
block, which expvar keys are flowing, and the current snapshot values
|
block, which expvar keys are flowing, and the current snapshot values
|
||||||
for each. Returns an empty state when metrics are not configured.
|
for each. Returns an empty state when metrics are not configured.
|
||||||
id: >-
|
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
|
||||||
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.
|
||||||
- content: >-
|
Same {t, v} shape as /history/:metric/:bucket.
|
||||||
Time-series history for one Xray runtime metric over the last ~6
|
id: time-series-history-for-one-xray-runtime-metric-over-the-last-6-hours-same-t-v-shape-as-historymetricbucket
|
||||||
hours. Same {t, v} shape as /history/:metric/:bucket.
|
- content: Latest snapshot from the Xray observatory — per-outbound latency,
|
||||||
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
|
health status, and last-probe time. Only populated when the Xray
|
||||||
config has an observatory configured.
|
config has an observatory configured.
|
||||||
id: >-
|
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
|
||||||
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,
|
||||||
- content: >-
|
v} shape as the other history endpoints.
|
||||||
Time-series of observatory probe results for one outbound tag. Same
|
id: time-series-of-observatory-probe-results-for-one-outbound-tag-same-t-v-shape-as-the-other-history-endpoints
|
||||||
{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.
|
- content: List Xray binary versions available for install on this host.
|
||||||
id: 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.
|
- content: Check whether a newer 3x-ui release is available on GitHub.
|
||||||
id: check-whether-a-newer-3x-ui-release-is-available-on-github
|
id: check-whether-a-newer-3x-ui-release-is-available-on-github
|
||||||
- content: >-
|
- content: Return the assembled Xray config that’s currently running on this host.
|
||||||
Return the assembled Xray config that’s currently running on this
|
|
||||||
host.
|
|
||||||
id: return-the-assembled-xray-config-thats-currently-running-on-this-host
|
id: return-the-assembled-xray-config-thats-currently-running-on-this-host
|
||||||
- content: >-
|
- content: Stream the SQLite database file as an attachment. Use as a manual
|
||||||
Stream the SQLite database file as an attachment. Use as a manual
|
|
||||||
backup.
|
backup.
|
||||||
id: >-
|
id: stream-the-sqlite-database-file-as-an-attachment-use-as-a-manual-backup
|
||||||
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 cross-engine migration file as an attachment: a .dump (SQL
|
|
||||||
text) on SQLite, or a .db SQLite database built from the live data on
|
text) on SQLite, or a .db SQLite database built from the live data on
|
||||||
PostgreSQL.
|
PostgreSQL.'
|
||||||
id: >-
|
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
|
||||||
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.
|
- content: Generate a fresh UUID v4. Convenience helper for client IDs.
|
||||||
id: generate-a-fresh-uuid-v4-convenience-helper-for-client-ids
|
id: generate-a-fresh-uuid-v4-convenience-helper-for-client-ids
|
||||||
- content: >-
|
- content: Return this panel's own web TLS certificate and key file paths. The
|
||||||
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
|
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
|
from Panel" fills a node-assigned inbound with paths that exist on the
|
||||||
node.
|
node.
|
||||||
id: >-
|
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
|
||||||
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)
|
||||||
- content: >-
|
of the nodes this panel manages. A parent panel calls it on a node
|
||||||
Read-only summaries (guid, parentGuid, name, address, status,
|
(via the node API token) to surface transitive sub-nodes in a chained
|
||||||
versions) of the nodes this panel manages. A parent panel calls it on
|
topology. Counts are computed by the parent, not returned here.
|
||||||
a node (via the node API token) to surface transitive sub-nodes in a
|
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
|
||||||
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.
|
- content: Generate a new X25519 keypair for Reality.
|
||||||
id: generate-a-new-x25519-keypair-for-reality
|
id: generate-a-new-x25519-keypair-for-reality
|
||||||
- content: >-
|
- content: Generate a new ML-DSA-65 keypair (post-quantum signature). Returns
|
||||||
Generate a new ML-DSA-65 keypair (post-quantum signature). Returns
|
|
||||||
{privateKey, publicKey, seed}.
|
{privateKey, publicKey, seed}.
|
||||||
id: >-
|
id: generate-a-new-ml-dsa-65-keypair-post-quantum-signature-returns-privatekey-publickey-seed
|
||||||
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
|
||||||
- content: >-
|
|
||||||
Generate a new ML-KEM-768 keypair (post-quantum KEM). Returns
|
|
||||||
{clientKey, serverKey}.
|
{clientKey, serverKey}.
|
||||||
id: >-
|
id: generate-a-new-ml-kem-768-keypair-post-quantum-kem-returns-clientkey-serverkey
|
||||||
generate-a-new-ml-kem-768-keypair-post-quantum-kem-returns-clientkey-serverkey
|
- content: Generate VLESS encryption auth options. Returns an auths array each
|
||||||
- content: >-
|
|
||||||
Generate VLESS encryption auth options. Returns an auths array each
|
|
||||||
with id, label, encryption, and decryption fields.
|
with id, label, encryption, and decryption fields.
|
||||||
id: >-
|
id: generate-vless-encryption-auth-options-returns-an-auths-array-each-with-id-label-encryption-and-decryption-fields
|
||||||
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.
|
- content: Stop the Xray binary. All proxies go offline immediately.
|
||||||
id: stop-the-xray-binary-all-proxies-go-offline-immediately
|
id: stop-the-xray-binary-all-proxies-go-offline-immediately
|
||||||
- content: >-
|
- content: Reload Xray with the current config. Typically required after
|
||||||
Reload Xray with the current config. Typically required after
|
|
||||||
structural inbound or routing changes.
|
structural inbound or routing changes.
|
||||||
id: >-
|
id: reload-xray-with-the-current-config-typically-required-after-structural-inbound-or-routing-changes
|
||||||
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
|
||||||
- content: >-
|
|
||||||
Download and install the specified Xray version. Pass "latest" for the
|
|
||||||
newest release.
|
newest release.
|
||||||
id: >-
|
id: download-and-install-the-specified-xray-version-pass-latest-for-the-newest-release
|
||||||
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
|
||||||
- content: >-
|
|
||||||
Self-update the panel to the latest version. The server restarts on
|
|
||||||
success.
|
success.
|
||||||
id: >-
|
id: self-update-the-panel-to-the-latest-version-the-server-restarts-on-success
|
||||||
self-update-the-panel-to-the-latest-version-the-server-restarts-on-success
|
- content: Toggle the panel update channel between stable and the rolling
|
||||||
- content: >-
|
|
||||||
Toggle the panel update channel between stable and the rolling
|
|
||||||
per-commit dev release. Only effective on dev builds.
|
per-commit dev release. Only effective on dev builds.
|
||||||
id: >-
|
id: toggle-the-panel-update-channel-between-stable-and-the-rolling-per-commit-dev-release-only-effective-on-dev-builds
|
||||||
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
|
||||||
- content: >-
|
|
||||||
Refresh the default GeoIP / GeoSite data files. Body can include a
|
|
||||||
fileName, or use the /:fileName variant.
|
fileName, or use the /:fileName variant.
|
||||||
id: >-
|
id: refresh-the-default-geoip--geosite-data-files-body-can-include-a-filename-or-use-the-filename-variant
|
||||||
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).
|
- 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
|
id: refresh-a-single-geo-file-by-filename-eg-geoipdat-geositedat
|
||||||
- content: Return the last N lines of the panel’s own log.
|
- content: Return the last N lines of the panel’s own log.
|
||||||
id: return-the-last-n-lines-of-the-panels-own-log
|
id: return-the-last-n-lines-of-the-panels-own-log
|
||||||
- content: Return the last N lines of the Xray process log.
|
- content: Return the last N lines of the Xray process log.
|
||||||
id: return-the-last-n-lines-of-the-xray-process-log
|
id: return-the-last-n-lines-of-the-xray-process-log
|
||||||
- content: >-
|
- content: Restore the panel DB from an uploaded SQLite file (multipart form,
|
||||||
Restore the panel DB from an uploaded SQLite file (multipart form,
|
|
||||||
field name "db"). The panel restarts after restore. Destructive.
|
field name "db"). The panel restarts after restore. Destructive.
|
||||||
id: >-
|
id: restore-the-panel-db-from-an-uploaded-sqlite-file-multipart-form-field-name-db-the-panel-restarts-after-restore-destructive
|
||||||
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
|
||||||
- content: >-
|
the given SNI.
|
||||||
Generate a new ECH (Encrypted Client Hello) keypair and config list
|
id: generate-a-new-ech-encrypted-client-hello-keypair-and-config-list-for-the-given-sni
|
||||||
for the given SNI.
|
- content: Compute the hex SHA-256 of a certificate (DER) for pinning
|
||||||
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
|
(pinnedPeerCertSha256). Provide either a server file path or inline
|
||||||
PEM/DER content.
|
PEM/DER content.
|
||||||
id: >-
|
id: compute-the-hex-sha-256-of-a-certificate-der-for-pinning-pinnedpeercertsha256-provide-either-a-server-file-path-or-inline-pemder-content
|
||||||
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
|
||||||
- content: >-
|
|
||||||
Run `xray tls ping` against a remote server and return its live
|
|
||||||
leaf-certificate SHA-256 hash(es) for pinning (pinnedPeerCertSha256).
|
leaf-certificate SHA-256 hash(es) for pinning (pinnedPeerCertSha256).
|
||||||
id: >-
|
id: run-xray-tls-ping-against-a-remote-server-and-return-its-live-leaf-certificate-sha-256-hashes-for-pinning-pinnedpeercertsha256
|
||||||
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
|
||||||
- content: >-
|
|
||||||
Fetch the fully aggregated inbound_client_ips database table. Used by
|
|
||||||
nodes to sync recently active IPs across the cluster.
|
nodes to sync recently active IPs across the cluster.
|
||||||
id: >-
|
id: fetch-the-fully-aggregated-inbound_client_ips-database-table-used-by-nodes-to-sync-recently-active-ips-across-the-cluster
|
||||||
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
|
||||||
- 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.
|
with the existing database to maintain a unified global IP-limit view.
|
||||||
id: >-
|
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
|
||||||
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: []
|
contents: []
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
---
|
---
|
||||||
title: Settings
|
title: Settings
|
||||||
description: >-
|
description: Panel configuration and user credentials. All endpoints live under
|
||||||
Panel configuration and user credentials. All endpoints live under
|
|
||||||
/panel/api/setting and require a logged-in session or Bearer token.
|
/panel/api/setting and require a logged-in session or Bearer token.
|
||||||
full: true
|
full: true
|
||||||
_openapi:
|
_openapi:
|
||||||
@@ -9,101 +8,69 @@ _openapi:
|
|||||||
- ./public/openapi.json
|
- ./public/openapi.json
|
||||||
toc:
|
toc:
|
||||||
- depth: 2
|
- depth: 2
|
||||||
title: >-
|
title: 'Return every panel setting: web server, Telegram bot, subscription,
|
||||||
Return every panel setting: web server, Telegram bot, subscription,
|
security, LDAP. The full JSON blob that the Settings page edits.'
|
||||||
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'
|
||||||
url: >-
|
|
||||||
#return-every-panel-setting-web-server-telegram-bot-subscription-security-ldap-the-full-json-blob-that-the-settings-page-edits
|
|
||||||
- depth: 2
|
- depth: 2
|
||||||
title: >-
|
title: Return the computed default settings based on the request host. Useful to
|
||||||
Return the computed default settings based on the request host. Useful
|
preview what a fresh install would use.
|
||||||
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'
|
||||||
url: >-
|
|
||||||
#return-the-computed-default-settings-based-on-the-request-host-useful-to-preview-what-a-fresh-install-would-use
|
|
||||||
- depth: 2
|
- depth: 2
|
||||||
title: >-
|
title: Persist every setting at once. The body mirrors the shape returned by
|
||||||
Persist every setting at once. The body mirrors the shape returned by
|
|
||||||
/all. Invalid values (bad ports, missing cert pairs, etc.) are rejected
|
/all. Invalid values (bad ports, missing cert pairs, etc.) are rejected
|
||||||
before write.
|
before write.
|
||||||
url: >-
|
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'
|
||||||
#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
|
- depth: 2
|
||||||
title: >-
|
title: Change the panel admin username and password. Requires the current
|
||||||
Change the panel admin username and password. Requires the current
|
|
||||||
credentials for verification. The session is refreshed with the new
|
credentials for verification. The session is refreshed with the new
|
||||||
values on success.
|
values on success.
|
||||||
url: >-
|
url: '#change-the-panel-admin-username-and-password-requires-the-current-credentials-for-verification-the-session-is-refreshed-with-the-new-values-on-success'
|
||||||
#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
|
- depth: 2
|
||||||
title: >-
|
title: Restart the entire 3x-ui process after a 3-second grace period. The
|
||||||
Restart the entire 3x-ui process after a 3-second grace period. The
|
|
||||||
connection drops immediately; the panel comes back online ~5-10 seconds
|
connection drops immediately; the panel comes back online ~5-10 seconds
|
||||||
later.
|
later.
|
||||||
url: >-
|
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'
|
||||||
#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
|
- depth: 2
|
||||||
title: >-
|
title: Test SMTP connection with stage-by-stage reporting (connect, auth, send).
|
||||||
Test SMTP connection with stage-by-stage reporting (connect, auth,
|
Returns structured result with stage and message.
|
||||||
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'
|
||||||
url: >-
|
|
||||||
#test-smtp-connection-with-stage-by-stage-reporting-connect-auth-send-returns-structured-result-with-stage-and-message
|
|
||||||
- depth: 2
|
- depth: 2
|
||||||
title: >-
|
title: Test Telegram bot connection by sending a test message to the configured
|
||||||
Test Telegram bot connection by sending a test message to the configured
|
|
||||||
chat.
|
chat.
|
||||||
url: >-
|
url: '#test-telegram-bot-connection-by-sending-a-test-message-to-the-configured-chat'
|
||||||
#test-telegram-bot-connection-by-sending-a-test-message-to-the-configured-chat
|
|
||||||
- depth: 2
|
- depth: 2
|
||||||
title: >-
|
title: Return the built-in default Xray JSON config template that ships with
|
||||||
Return the built-in default Xray JSON config template that ships with
|
|
||||||
this panel version.
|
this panel version.
|
||||||
url: >-
|
url: '#return-the-built-in-default-xray-json-config-template-that-ships-with-this-panel-version'
|
||||||
#return-the-built-in-default-xray-json-config-template-that-ships-with-this-panel-version
|
|
||||||
structuredData:
|
structuredData:
|
||||||
headings:
|
headings:
|
||||||
- content: >-
|
- content: 'Return every panel setting: web server, Telegram bot, subscription,
|
||||||
Return every panel setting: web server, Telegram bot, subscription,
|
security, LDAP. The full JSON blob that the Settings page edits.'
|
||||||
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
|
||||||
id: >-
|
- content: Return the computed default settings based on the request host. Useful
|
||||||
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.
|
to preview what a fresh install would use.
|
||||||
id: >-
|
id: return-the-computed-default-settings-based-on-the-request-host-useful-to-preview-what-a-fresh-install-would-use
|
||||||
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
|
||||||
- content: >-
|
|
||||||
Persist every setting at once. The body mirrors the shape returned by
|
|
||||||
/all. Invalid values (bad ports, missing cert pairs, etc.) are
|
/all. Invalid values (bad ports, missing cert pairs, etc.) are
|
||||||
rejected before write.
|
rejected before write.
|
||||||
id: >-
|
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
|
||||||
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
|
||||||
- content: >-
|
|
||||||
Change the panel admin username and password. Requires the current
|
|
||||||
credentials for verification. The session is refreshed with the new
|
credentials for verification. The session is refreshed with the new
|
||||||
values on success.
|
values on success.
|
||||||
id: >-
|
id: change-the-panel-admin-username-and-password-requires-the-current-credentials-for-verification-the-session-is-refreshed-with-the-new-values-on-success
|
||||||
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
|
||||||
- content: >-
|
|
||||||
Restart the entire 3x-ui process after a 3-second grace period. The
|
|
||||||
connection drops immediately; the panel comes back online ~5-10
|
connection drops immediately; the panel comes back online ~5-10
|
||||||
seconds later.
|
seconds later.
|
||||||
id: >-
|
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
|
||||||
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,
|
||||||
- content: >-
|
|
||||||
Test SMTP connection with stage-by-stage reporting (connect, auth,
|
|
||||||
send). Returns structured result with stage and message.
|
send). Returns structured result with stage and message.
|
||||||
id: >-
|
id: test-smtp-connection-with-stage-by-stage-reporting-connect-auth-send-returns-structured-result-with-stage-and-message
|
||||||
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
|
||||||
- content: >-
|
|
||||||
Test Telegram bot connection by sending a test message to the
|
|
||||||
configured chat.
|
configured chat.
|
||||||
id: >-
|
id: test-telegram-bot-connection-by-sending-a-test-message-to-the-configured-chat
|
||||||
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
|
||||||
- content: >-
|
|
||||||
Return the built-in default Xray JSON config template that ships with
|
|
||||||
this panel version.
|
this panel version.
|
||||||
id: >-
|
id: return-the-built-in-default-xray-json-config-template-that-ships-with-this-panel-version
|
||||||
return-the-built-in-default-xray-json-config-template-that-ships-with-this-panel-version
|
|
||||||
contents: []
|
contents: []
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -1,59 +1,46 @@
|
|||||||
---
|
---
|
||||||
title: Subscription Server
|
title: Subscription Server
|
||||||
description: >-
|
description: A separate HTTP/HTTPS server that serves proxy subscription links
|
||||||
A separate HTTP/HTTPS server that serves proxy subscription links (standard,
|
(standard, JSON, and Clash) to clients. The server listens on its own port
|
||||||
JSON, and Clash) to clients. The server listens on its own port (default
|
(default 10882) and is configured in Settings → Subscription. Paths are
|
||||||
10882) and is configured in Settings → Subscription. Paths are configurable;
|
configurable; defaults are shown below. All subscription endpoints set
|
||||||
defaults are shown below. All subscription endpoints set response headers for
|
response headers for client apps to read traffic/expiry info.
|
||||||
client apps to read traffic/expiry info.
|
|
||||||
full: true
|
full: true
|
||||||
_openapi:
|
_openapi:
|
||||||
preload:
|
preload:
|
||||||
- ./public/openapi.json
|
- ./public/openapi.json
|
||||||
toc:
|
toc:
|
||||||
- depth: 2
|
- depth: 2
|
||||||
title: >-
|
title: 'Return base64-encoded subscription links for all enabled clients
|
||||||
Return base64-encoded subscription links for all enabled clients
|
|
||||||
matching the subscription ID. When the request has an Accept: text/html
|
matching the subscription ID. When the request has an Accept: text/html
|
||||||
header or ?html=1, renders a styled info page instead. Default path:
|
header or ?html=1, renders a styled info page instead. Default path:
|
||||||
/sub/:subid.
|
/sub/:subid.'
|
||||||
url: >-
|
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'
|
||||||
#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
|
|
||||||
- depth: 2
|
- depth: 2
|
||||||
title: >-
|
title: 'Return subscription as a JSON array of proxy configs (one per enabled
|
||||||
Return subscription as a JSON array of proxy configs (one per enabled
|
|
||||||
client). Only when JSON subscription is enabled in settings. Default
|
client). Only when JSON subscription is enabled in settings. Default
|
||||||
path: /json/:subid.
|
path: /json/:subid.'
|
||||||
url: >-
|
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'
|
||||||
#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
|
- depth: 2
|
||||||
title: >-
|
title: 'Return subscription as a Clash/Mihomo-compatible YAML config, including
|
||||||
Return subscription as a Clash/Mihomo-compatible YAML config, including
|
|
||||||
configured global Clash routing rules. Only when Clash subscription is
|
configured global Clash routing rules. Only when Clash subscription is
|
||||||
enabled in settings. Default path: /clash/:subid.
|
enabled in settings. Default path: /clash/:subid.'
|
||||||
url: >-
|
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'
|
||||||
#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:
|
structuredData:
|
||||||
headings:
|
headings:
|
||||||
- content: >-
|
- content: 'Return base64-encoded subscription links for all enabled clients
|
||||||
Return base64-encoded subscription links for all enabled clients
|
|
||||||
matching the subscription ID. When the request has an Accept:
|
matching the subscription ID. When the request has an Accept:
|
||||||
text/html header or ?html=1, renders a styled info page instead.
|
text/html header or ?html=1, renders a styled info page instead.
|
||||||
Default path: /sub/:subid.
|
Default path: /sub/:subid.'
|
||||||
id: >-
|
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
|
||||||
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
|
||||||
- content: >-
|
|
||||||
Return subscription as a JSON array of proxy configs (one per enabled
|
|
||||||
client). Only when JSON subscription is enabled in settings. Default
|
client). Only when JSON subscription is enabled in settings. Default
|
||||||
path: /json/:subid.
|
path: /json/:subid.'
|
||||||
id: >-
|
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
|
||||||
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,
|
||||||
- content: >-
|
|
||||||
Return subscription as a Clash/Mihomo-compatible YAML config,
|
|
||||||
including configured global Clash routing rules. Only when Clash
|
including configured global Clash routing rules. Only when Clash
|
||||||
subscription is enabled in settings. Default path: /clash/:subid.
|
subscription is enabled in settings. Default path: /clash/:subid.'
|
||||||
id: >-
|
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
|
||||||
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: []
|
contents: []
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
---
|
---
|
||||||
title: WebSocket
|
title: WebSocket
|
||||||
description: >-
|
description: Real-time status updates via WebSocket. Connect once at
|
||||||
Real-time status updates via WebSocket. Connect once at
|
|
||||||
<code>ws://<panel>/ws</code> to receive a stream of JSON messages without
|
<code>ws://<panel>/ws</code> to receive a stream of JSON messages without
|
||||||
polling. Requires an authenticated session cookie (Bearer token auth is not
|
polling. Requires an authenticated session cookie (Bearer token auth is not
|
||||||
supported). Each message has a <code>type</code> field that identifies the
|
supported). Each message has a <code>type</code> field that identifies the
|
||||||
@@ -12,22 +11,18 @@ _openapi:
|
|||||||
- ./public/openapi.json
|
- ./public/openapi.json
|
||||||
toc:
|
toc:
|
||||||
- depth: 2
|
- depth: 2
|
||||||
title: >-
|
title: Upgrade an HTTP connection to a WebSocket. Requires an authenticated
|
||||||
Upgrade an HTTP connection to a WebSocket. Requires an authenticated
|
|
||||||
session cookie (Bearer token auth is not supported here). Returns 101
|
session cookie (Bearer token auth is not supported here). Returns 101
|
||||||
Switching Protocols on success. The server then pushes JSON messages
|
Switching Protocols on success. The server then pushes JSON messages
|
||||||
described below.
|
described below.
|
||||||
url: >-
|
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'
|
||||||
#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:
|
structuredData:
|
||||||
headings:
|
headings:
|
||||||
- content: >-
|
- content: Upgrade an HTTP connection to a WebSocket. Requires an authenticated
|
||||||
Upgrade an HTTP connection to a WebSocket. Requires an authenticated
|
|
||||||
session cookie (Bearer token auth is not supported here). Returns 101
|
session cookie (Bearer token auth is not supported here). Returns 101
|
||||||
Switching Protocols on success. The server then pushes JSON messages
|
Switching Protocols on success. The server then pushes JSON messages
|
||||||
described below.
|
described below.
|
||||||
id: >-
|
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
|
||||||
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: []
|
contents: []
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -1,50 +1,37 @@
|
|||||||
---
|
---
|
||||||
title: Xray Settings
|
title: Xray Settings
|
||||||
description: >-
|
description: Xray configuration template, outbound management, Warp/Nord
|
||||||
Xray configuration template, outbound management, Warp/Nord integration, and
|
integration, and config testing. All endpoints under /panel/api/xray.
|
||||||
config testing. All endpoints under /panel/api/xray.
|
|
||||||
full: true
|
full: true
|
||||||
_openapi:
|
_openapi:
|
||||||
preload:
|
preload:
|
||||||
- ./public/openapi.json
|
- ./public/openapi.json
|
||||||
toc:
|
toc:
|
||||||
- depth: 2
|
- depth: 2
|
||||||
title: >-
|
title: Return the Xray config template (JSON string), available inbound tags,
|
||||||
Return the Xray config template (JSON string), available inbound tags,
|
|
||||||
client reverse tags, and the configured outbound test URL in one
|
client reverse tags, and the configured outbound test URL in one
|
||||||
response.
|
response.
|
||||||
url: >-
|
url: '#return-the-xray-config-template-json-string-available-inbound-tags-client-reverse-tags-and-the-configured-outbound-test-url-in-one-response'
|
||||||
#return-the-xray-config-template-json-string-available-inbound-tags-client-reverse-tags-and-the-configured-outbound-test-url-in-one-response
|
|
||||||
- depth: 2
|
- depth: 2
|
||||||
title: >-
|
title: Return the built-in default Xray config shipped with the panel (identical
|
||||||
Return the built-in default Xray config shipped with the panel
|
to /panel/api/setting/getDefaultJsonConfig).
|
||||||
(identical to /panel/api/setting/getDefaultJsonConfig).
|
url: '#return-the-built-in-default-xray-config-shipped-with-the-panel-identical-to-panelapisettinggetdefaultjsonconfig'
|
||||||
url: >-
|
|
||||||
#return-the-built-in-default-xray-config-shipped-with-the-panel-identical-to-panelapisettinggetdefaultjsonconfig
|
|
||||||
- depth: 2
|
- depth: 2
|
||||||
title: >-
|
title: Return traffic statistics for every outbound. Each outbound shows
|
||||||
Return traffic statistics for every outbound. Each outbound shows
|
|
||||||
up/down/total counters.
|
up/down/total counters.
|
||||||
url: >-
|
url: '#return-traffic-statistics-for-every-outbound-each-outbound-shows-updowntotal-counters'
|
||||||
#return-traffic-statistics-for-every-outbound-each-outbound-shows-updowntotal-counters
|
|
||||||
- depth: 2
|
- depth: 2
|
||||||
title: >-
|
title: Return the most recent Xray process stdout/stderr output. Useful to check
|
||||||
Return the most recent Xray process stdout/stderr output. Useful to
|
for startup errors or runtime warnings.
|
||||||
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'
|
||||||
url: >-
|
|
||||||
#return-the-most-recent-xray-process-stdoutstderr-output-useful-to-check-for-startup-errors-or-runtime-warnings
|
|
||||||
- depth: 2
|
- depth: 2
|
||||||
title: >-
|
title: Save the Xray JSON config template and optionally the outbound test URL.
|
||||||
Save the Xray JSON config template and optionally the outbound test URL.
|
|
||||||
Both are sent as form fields.
|
Both are sent as form fields.
|
||||||
url: >-
|
url: '#save-the-xray-json-config-template-and-optionally-the-outbound-test-url-both-are-sent-as-form-fields'
|
||||||
#save-the-xray-json-config-template-and-optionally-the-outbound-test-url-both-are-sent-as-form-fields
|
|
||||||
- depth: 2
|
- depth: 2
|
||||||
title: >-
|
title: Manage Cloudflare Warp integration. The action parameter selects the
|
||||||
Manage Cloudflare Warp integration. The action parameter selects the
|
|
||||||
operation.
|
operation.
|
||||||
url: >-
|
url: '#manage-cloudflare-warp-integration-the-action-parameter-selects-the-operation'
|
||||||
#manage-cloudflare-warp-integration-the-action-parameter-selects-the-operation
|
|
||||||
- depth: 2
|
- depth: 2
|
||||||
title: Manage NordVPN integration. The action parameter selects the operation.
|
title: Manage NordVPN integration. The action parameter selects the operation.
|
||||||
url: '#manage-nordvpn-integration-the-action-parameter-selects-the-operation'
|
url: '#manage-nordvpn-integration-the-action-parameter-selects-the-operation'
|
||||||
@@ -52,193 +39,130 @@ _openapi:
|
|||||||
title: Reset traffic counters for a specific outbound by tag.
|
title: Reset traffic counters for a specific outbound by tag.
|
||||||
url: '#reset-traffic-counters-for-a-specific-outbound-by-tag'
|
url: '#reset-traffic-counters-for-a-specific-outbound-by-tag'
|
||||||
- depth: 2
|
- depth: 2
|
||||||
title: >-
|
title: Test an outbound configuration. Sends the outbound JSON (required),
|
||||||
Test an outbound configuration. Sends the outbound JSON (required),
|
|
||||||
optionally all outbounds (to resolve sockopt.dialerProxy dependencies),
|
optionally all outbounds (to resolve sockopt.dialerProxy dependencies),
|
||||||
and a mode flag.
|
and a mode flag.
|
||||||
url: >-
|
url: '#test-an-outbound-configuration-sends-the-outbound-json-required-optionally-all-outbounds-to-resolve-sockoptdialerproxy-dependencies-and-a-mode-flag'
|
||||||
#test-an-outbound-configuration-sends-the-outbound-json-required-optionally-all-outbounds-to-resolve-sockoptdialerproxy-dependencies-and-a-mode-flag
|
|
||||||
- depth: 2
|
- depth: 2
|
||||||
title: >-
|
title: Test a batch of outbounds (max 50) through one shared temp xray instance.
|
||||||
Test a batch of outbounds (max 50) through one shared temp xray
|
Returns an array of results in input order, each with the outbound tag,
|
||||||
instance. Returns an array of results in input order, each with the
|
delay, HTTP status and a connect/TLS/TTFB timing breakdown.
|
||||||
outbound tag, delay, HTTP status and a connect/TLS/TTFB timing
|
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'
|
||||||
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
|
- depth: 2
|
||||||
title: >-
|
title: 'Live state of routing balancers in the running core
|
||||||
Live state of routing balancers in the running core
|
|
||||||
(RoutingService.GetBalancerInfo): current override and the targets the
|
(RoutingService.GetBalancerInfo): 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: >-
|
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'
|
||||||
#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
|
- depth: 2
|
||||||
title: >-
|
title: Force a balancer in the running core to always pick one outbound
|
||||||
Force a balancer in the running core to always pick one outbound
|
|
||||||
(RoutingService.OverrideBalancerTarget). Applied live without a restart;
|
(RoutingService.OverrideBalancerTarget). Applied live without a restart;
|
||||||
cleared automatically when Xray restarts.
|
cleared automatically when Xray restarts.
|
||||||
url: >-
|
url: '#force-a-balancer-in-the-running-core-to-always-pick-one-outbound-routingserviceoverridebalancertarget-applied-live-without-a-restart-cleared-automatically-when-xray-restarts'
|
||||||
#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
|
- depth: 2
|
||||||
title: >-
|
title: Ask the running core which outbound its router would pick for a synthetic
|
||||||
Ask the running core which outbound its router would pick for a
|
connection (RoutingService.TestRoute). No traffic is sent.
|
||||||
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'
|
||||||
url: >-
|
|
||||||
#ask-the-running-core-which-outbound-its-router-would-pick-for-a-synthetic-connection-routingservicetestroute-no-traffic-is-sent
|
|
||||||
- depth: 2
|
- depth: 2
|
||||||
title: >-
|
title: List all outbound subscriptions (remote URLs that supply additional
|
||||||
List all outbound subscriptions (remote URLs that supply additional
|
|
||||||
outbounds), newest first.
|
outbounds), newest first.
|
||||||
url: >-
|
url: '#list-all-outbound-subscriptions-remote-urls-that-supply-additional-outbounds-newest-first'
|
||||||
#list-all-outbound-subscriptions-remote-urls-that-supply-additional-outbounds-newest-first
|
|
||||||
- depth: 2
|
- depth: 2
|
||||||
title: >-
|
title: Create an outbound subscription. The URL is fetched, parsed into
|
||||||
Create an outbound subscription. The URL is fetched, parsed into
|
|
||||||
outbounds with stable tags, and merged additively into the running Xray
|
outbounds with stable tags, and merged additively into the running Xray
|
||||||
config.
|
config.
|
||||||
url: >-
|
url: '#create-an-outbound-subscription-the-url-is-fetched-parsed-into-outbounds-with-stable-tags-and-merged-additively-into-the-running-xray-config'
|
||||||
#create-an-outbound-subscription-the-url-is-fetched-parsed-into-outbounds-with-stable-tags-and-merged-additively-into-the-running-xray-config
|
|
||||||
- depth: 2
|
- depth: 2
|
||||||
title: >-
|
title: Update an existing outbound subscription by id. Accepts the same form
|
||||||
Update an existing outbound subscription by id. Accepts the same form
|
|
||||||
fields as create.
|
fields as create.
|
||||||
url: >-
|
url: '#update-an-existing-outbound-subscription-by-id-accepts-the-same-form-fields-as-create'
|
||||||
#update-an-existing-outbound-subscription-by-id-accepts-the-same-form-fields-as-create
|
|
||||||
- depth: 2
|
- depth: 2
|
||||||
title: Delete an outbound subscription by id.
|
title: Delete an outbound subscription by id.
|
||||||
url: '#delete-an-outbound-subscription-by-id'
|
url: '#delete-an-outbound-subscription-by-id'
|
||||||
- depth: 2
|
- depth: 2
|
||||||
title: >-
|
title: Delete an outbound subscription by id (POST alias of DELETE for
|
||||||
Delete an outbound subscription by id (POST alias of DELETE for
|
|
||||||
axios-friendly clients).
|
axios-friendly clients).
|
||||||
url: >-
|
url: '#delete-an-outbound-subscription-by-id-post-alias-of-delete-for-axios-friendly-clients'
|
||||||
#delete-an-outbound-subscription-by-id-post-alias-of-delete-for-axios-friendly-clients
|
|
||||||
- depth: 2
|
- depth: 2
|
||||||
title: >-
|
title: Force an immediate re-fetch of the subscription and return the parsed
|
||||||
Force an immediate re-fetch of the subscription and return the parsed
|
|
||||||
outbounds. Signals Xray to reload.
|
outbounds. Signals Xray to reload.
|
||||||
url: >-
|
url: '#force-an-immediate-re-fetch-of-the-subscription-and-return-the-parsed-outbounds-signals-xray-to-reload'
|
||||||
#force-an-immediate-re-fetch-of-the-subscription-and-return-the-parsed-outbounds-signals-xray-to-reload
|
|
||||||
- depth: 2
|
- depth: 2
|
||||||
title: >-
|
title: Reorder a subscription one step up or down in priority (controls its
|
||||||
Reorder a subscription one step up or down in priority (controls its
|
|
||||||
position in the merged outbounds).
|
position in the merged outbounds).
|
||||||
url: >-
|
url: '#reorder-a-subscription-one-step-up-or-down-in-priority-controls-its-position-in-the-merged-outbounds'
|
||||||
#reorder-a-subscription-one-step-up-or-down-in-priority-controls-its-position-in-the-merged-outbounds
|
|
||||||
- depth: 2
|
- depth: 2
|
||||||
title: >-
|
title: 'Preview a subscription URL: fetch and parse it into outbounds without
|
||||||
Preview a subscription URL: fetch and parse it into outbounds without
|
persisting anything.'
|
||||||
persisting anything.
|
url: '#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:
|
structuredData:
|
||||||
headings:
|
headings:
|
||||||
- content: >-
|
- content: Return the Xray config template (JSON string), available inbound tags,
|
||||||
Return the Xray config template (JSON string), available inbound tags,
|
|
||||||
client reverse tags, and the configured outbound test URL in one
|
client reverse tags, and the configured outbound test URL in one
|
||||||
response.
|
response.
|
||||||
id: >-
|
id: return-the-xray-config-template-json-string-available-inbound-tags-client-reverse-tags-and-the-configured-outbound-test-url-in-one-response
|
||||||
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
|
||||||
- content: >-
|
|
||||||
Return the built-in default Xray config shipped with the panel
|
|
||||||
(identical to /panel/api/setting/getDefaultJsonConfig).
|
(identical to /panel/api/setting/getDefaultJsonConfig).
|
||||||
id: >-
|
id: return-the-built-in-default-xray-config-shipped-with-the-panel-identical-to-panelapisettinggetdefaultjsonconfig
|
||||||
return-the-built-in-default-xray-config-shipped-with-the-panel-identical-to-panelapisettinggetdefaultjsonconfig
|
- content: Return traffic statistics for every outbound. Each outbound shows
|
||||||
- content: >-
|
|
||||||
Return traffic statistics for every outbound. Each outbound shows
|
|
||||||
up/down/total counters.
|
up/down/total counters.
|
||||||
id: >-
|
id: return-traffic-statistics-for-every-outbound-each-outbound-shows-updowntotal-counters
|
||||||
return-traffic-statistics-for-every-outbound-each-outbound-shows-updowntotal-counters
|
- content: Return the most recent Xray process stdout/stderr output. Useful to
|
||||||
- content: >-
|
|
||||||
Return the most recent Xray process stdout/stderr output. Useful to
|
|
||||||
check for startup errors or runtime warnings.
|
check for startup errors or runtime warnings.
|
||||||
id: >-
|
id: return-the-most-recent-xray-process-stdoutstderr-output-useful-to-check-for-startup-errors-or-runtime-warnings
|
||||||
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
|
||||||
- content: >-
|
|
||||||
Save the Xray JSON config template and optionally the outbound test
|
|
||||||
URL. Both are sent as form fields.
|
URL. Both are sent as form fields.
|
||||||
id: >-
|
id: save-the-xray-json-config-template-and-optionally-the-outbound-test-url-both-are-sent-as-form-fields
|
||||||
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
|
||||||
- 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.
|
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
|
id: manage-nordvpn-integration-the-action-parameter-selects-the-operation
|
||||||
- content: Reset traffic counters for a specific outbound by tag.
|
- content: Reset traffic counters for a specific outbound by tag.
|
||||||
id: reset-traffic-counters-for-a-specific-outbound-by-tag
|
id: reset-traffic-counters-for-a-specific-outbound-by-tag
|
||||||
- content: >-
|
- content: Test an outbound configuration. Sends the outbound JSON (required),
|
||||||
Test an outbound configuration. Sends the outbound JSON (required),
|
|
||||||
optionally all outbounds (to resolve sockopt.dialerProxy
|
optionally all outbounds (to resolve sockopt.dialerProxy
|
||||||
dependencies), and a mode flag.
|
dependencies), and a mode flag.
|
||||||
id: >-
|
id: test-an-outbound-configuration-sends-the-outbound-json-required-optionally-all-outbounds-to-resolve-sockoptdialerproxy-dependencies-and-a-mode-flag
|
||||||
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
|
||||||
- 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
|
instance. Returns an array of results in input order, each with the
|
||||||
outbound tag, delay, HTTP status and a connect/TLS/TTFB timing
|
outbound tag, delay, HTTP status and a connect/TLS/TTFB timing
|
||||||
breakdown.
|
breakdown.
|
||||||
id: >-
|
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
|
||||||
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
|
||||||
- content: >-
|
|
||||||
Live state of routing balancers in the running core
|
|
||||||
(RoutingService.GetBalancerInfo): current override and the targets the
|
(RoutingService.GetBalancerInfo): current override and the targets the
|
||||||
strategy prefers. Returns a map keyed by balancer tag.
|
strategy prefers. Returns a map keyed by balancer tag.'
|
||||||
id: >-
|
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
|
||||||
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
|
||||||
- content: >-
|
|
||||||
Force a balancer in the running core to always pick one outbound
|
|
||||||
(RoutingService.OverrideBalancerTarget). Applied live without a
|
(RoutingService.OverrideBalancerTarget). Applied live without a
|
||||||
restart; cleared automatically when Xray restarts.
|
restart; cleared automatically when Xray restarts.
|
||||||
id: >-
|
id: force-a-balancer-in-the-running-core-to-always-pick-one-outbound-routingserviceoverridebalancertarget-applied-live-without-a-restart-cleared-automatically-when-xray-restarts
|
||||||
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
|
||||||
- content: >-
|
|
||||||
Ask the running core which outbound its router would pick for a
|
|
||||||
synthetic connection (RoutingService.TestRoute). No traffic is sent.
|
synthetic connection (RoutingService.TestRoute). No traffic is sent.
|
||||||
id: >-
|
id: ask-the-running-core-which-outbound-its-router-would-pick-for-a-synthetic-connection-routingservicetestroute-no-traffic-is-sent
|
||||||
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
|
||||||
- content: >-
|
|
||||||
List all outbound subscriptions (remote URLs that supply additional
|
|
||||||
outbounds), newest first.
|
outbounds), newest first.
|
||||||
id: >-
|
id: list-all-outbound-subscriptions-remote-urls-that-supply-additional-outbounds-newest-first
|
||||||
list-all-outbound-subscriptions-remote-urls-that-supply-additional-outbounds-newest-first
|
- content: Create an outbound subscription. The URL is fetched, parsed into
|
||||||
- content: >-
|
|
||||||
Create an outbound subscription. The URL is fetched, parsed into
|
|
||||||
outbounds with stable tags, and merged additively into the running
|
outbounds with stable tags, and merged additively into the running
|
||||||
Xray config.
|
Xray config.
|
||||||
id: >-
|
id: create-an-outbound-subscription-the-url-is-fetched-parsed-into-outbounds-with-stable-tags-and-merged-additively-into-the-running-xray-config
|
||||||
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
|
||||||
- content: >-
|
|
||||||
Update an existing outbound subscription by id. Accepts the same form
|
|
||||||
fields as create.
|
fields as create.
|
||||||
id: >-
|
id: update-an-existing-outbound-subscription-by-id-accepts-the-same-form-fields-as-create
|
||||||
update-an-existing-outbound-subscription-by-id-accepts-the-same-form-fields-as-create
|
|
||||||
- content: Delete an outbound subscription by id.
|
- content: Delete an outbound subscription by id.
|
||||||
id: delete-an-outbound-subscription-by-id
|
id: delete-an-outbound-subscription-by-id
|
||||||
- content: >-
|
- content: Delete an outbound subscription by id (POST alias of DELETE for
|
||||||
Delete an outbound subscription by id (POST alias of DELETE for
|
|
||||||
axios-friendly clients).
|
axios-friendly clients).
|
||||||
id: >-
|
id: delete-an-outbound-subscription-by-id-post-alias-of-delete-for-axios-friendly-clients
|
||||||
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: >-
|
|
||||||
Force an immediate re-fetch of the subscription and return the parsed
|
|
||||||
outbounds. Signals Xray to reload.
|
outbounds. Signals Xray to reload.
|
||||||
id: >-
|
id: force-an-immediate-re-fetch-of-the-subscription-and-return-the-parsed-outbounds-signals-xray-to-reload
|
||||||
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
|
||||||
- content: >-
|
|
||||||
Reorder a subscription one step up or down in priority (controls its
|
|
||||||
position in the merged outbounds).
|
position in the merged outbounds).
|
||||||
id: >-
|
id: reorder-a-subscription-one-step-up-or-down-in-priority-controls-its-position-in-the-merged-outbounds
|
||||||
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
|
||||||
- content: >-
|
persisting anything.'
|
||||||
Preview a subscription URL: fetch and parse it into outbounds without
|
id: preview-a-subscription-url-fetch-and-parse-it-into-outbounds-without-persisting-anything
|
||||||
persisting anything.
|
|
||||||
id: >-
|
|
||||||
preview-a-subscription-url-fetch-and-parse-it-into-outbounds-without-persisting-anything
|
|
||||||
contents: []
|
contents: []
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -40,6 +40,9 @@ TLS یا REALITY) را انتخاب کنید. به [انتقالها](/docs/c
|
|||||||
بهصورت اختیاری میتوانید کل ترافیک را محدود کنید و یک تاریخ انقضا برای ورودی
|
بهصورت اختیاری میتوانید کل ترافیک را محدود کنید و یک تاریخ انقضا برای ورودی
|
||||||
تعیین کنید، و یک زمانبندی **بازنشانی ترافیک** دورهای انتخاب کنید: `never`
|
تعیین کنید، و یک زمانبندی **بازنشانی ترافیک** دورهای انتخاب کنید: `never`
|
||||||
(پیشفرض)، `hourly`، `daily`، `weekly` یا `monthly`.
|
(پیشفرض)، `hourly`، `daily`، `weekly` یا `monthly`.
|
||||||
|
|
||||||
|
برای بازنشانی `monthly`، روزی از ۱ تا ۳۱ انتخاب کنید. اگر آن روز در ماهی کوتاهتر
|
||||||
|
وجود نداشته باشد، بازنشانی در آخرین روز همان ماه انجام میشود.
|
||||||
</Step>
|
</Step>
|
||||||
|
|
||||||
</Steps>
|
</Steps>
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ icon: SlidersHorizontal
|
|||||||
| `webBasePath` | `/` | مسیر URLی که پنل زیر آن ارائه میشود (همیشه به شکل `/…/` نرمالسازی میشود). |
|
| `webBasePath` | `/` | مسیر URLی که پنل زیر آن ارائه میشود (همیشه به شکل `/…/` نرمالسازی میشود). |
|
||||||
| `webCertFile` / `webKeyFile` | _(هیچکدام)_ | گواهی + کلید TLS. وقتی هر دو تنظیم شوند، پنل با **HTTPS** ارائه میشود. |
|
| `webCertFile` / `webKeyFile` | _(هیچکدام)_ | گواهی + کلید TLS. وقتی هر دو تنظیم شوند، پنل با **HTTPS** ارائه میشود. |
|
||||||
| `sessionMaxAge` | `360` | طول عمر نشست بر حسب **دقیقه** (پیشفرض ۶ ساعت). |
|
| `sessionMaxAge` | `360` | طول عمر نشست بر حسب **دقیقه** (پیشفرض ۶ ساعت). |
|
||||||
| `trustedProxyCIDRs` | `127.0.0.1/32,::1/128` | IPها/CIDRهایی که هدرهای فورواردشدهشان (IP واقعی کلاینت) مورد اعتماد است. |
|
| `trustedProxyCIDRs` | `127.0.0.1/32,::1/128` | IPها/CIDRهایی که هدرهای فورواردشدهشان (IP واقعی کلاینت) مورد اعتماد است. مقدار سفارشی همچنین میزبان و طرحِ لینکهای اشتراک را کنترل میکند؛ پراکسی اشتراک را اضافه کنید یا برای بازنویسی این لینکها `subURI` را تنظیم کنید. |
|
||||||
| `panelOutbound` | _(هیچکدام)_ | مسیریابی خروجیِ خود پنل (بررسی بهروزرسانیها، Telegram، واکشی geo/sub) از طریق یک خروجی Xray با نام مشخص. |
|
| `panelOutbound` | _(هیچکدام)_ | مسیریابی خروجیِ خود پنل (بررسی بهروزرسانیها، Telegram، واکشی geo/sub) از طریق یک خروجی Xray با نام مشخص. |
|
||||||
|
|
||||||
پس از تغییر پورت یا مسیر پایه، آدرس پنل بهصورت
|
پس از تغییر پورت یا مسیر پایه، آدرس پنل بهصورت
|
||||||
|
|||||||
@@ -118,6 +118,13 @@ vless://<uuid>@<server>:443?security=reality&pbk=<public-key>&sid=<short-id>&sni
|
|||||||
- **نشت کلید خصوصی.** فقط و فقط **کلید عمومی** را میان کلاینتها توزیع کنید.
|
- **نشت کلید خصوصی.** فقط و فقط **کلید عمومی** را میان کلاینتها توزیع کنید.
|
||||||
- **جریان نادرست.** REALITY + XTLS-Vision به `flow = xtls-rprx-vision` هم در ورودیِ
|
- **جریان نادرست.** REALITY + XTLS-Vision به `flow = xtls-rprx-vision` هم در ورودیِ
|
||||||
مدخل کلاینت و هم در لینک اشتراکگذاری نیاز دارد.
|
مدخل کلاینت و هم در لینک اشتراکگذاری نیاز دارد.
|
||||||
|
- **هستههای قدیمی کلاینت بهطور پیشفرض رد میشوند.** خالی گذاشتن
|
||||||
|
**حداقل نسخه کلاینت** به معنای «بدون محدودیت» نیست: Xray-core به حداقل داخلیِ
|
||||||
|
نسخهٔ هستهای که اجرا میکنید (در نسخههای فعلی 26.3.27) بازمیگردد تا اثر انگشتهای TLS کلاینتها تازه
|
||||||
|
بمانند؛ در نتیجه هستههای شخص ثالث مانند Mihomo و sing-box حتی با پیکربندی
|
||||||
|
کاملاً درست در تأیید REALITY شکست میخورند — کلاینتها تایماوت میبینند و فقط
|
||||||
|
اپلیکیشنهای مبتنی بر Xray-core وصل میشوند. تنها در صورت نیاز به پشتیبانی از
|
||||||
|
آنها مقدار `1.0.0` را تنظیم کنید؛ این کار اثر انگشتهای قدیمی را هم میپذیرد.
|
||||||
|
|
||||||
</Callout>
|
</Callout>
|
||||||
|
|
||||||
|
|||||||
@@ -41,6 +41,9 @@ icon: ArrowDownToLine
|
|||||||
При необходимости ограничьте общий объём трафика и установите дату истечения для
|
При необходимости ограничьте общий объём трафика и установите дату истечения для
|
||||||
входящего подключения, а также выберите расписание периодического **сброса трафика**:
|
входящего подключения, а также выберите расписание периодического **сброса трафика**:
|
||||||
`never` (по умолчанию), `hourly`, `daily`, `weekly` или `monthly`.
|
`never` (по умолчанию), `hourly`, `daily`, `weekly` или `monthly`.
|
||||||
|
|
||||||
|
Для сброса `monthly` выберите день от 1 до 31. Если выбранного дня нет в более
|
||||||
|
коротком месяце, сброс выполняется в последний день этого месяца.
|
||||||
</Step>
|
</Step>
|
||||||
|
|
||||||
</Steps>
|
</Steps>
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ icon: SlidersHorizontal
|
|||||||
| `webBasePath` | `/` | URL-путь, по которому обслуживается панель (всегда нормализуется к `/…/`). |
|
| `webBasePath` | `/` | URL-путь, по которому обслуживается панель (всегда нормализуется к `/…/`). |
|
||||||
| `webCertFile` / `webKeyFile` | _(нет)_ | Сертификат TLS + ключ. Когда заданы оба, панель обслуживается по **HTTPS**. |
|
| `webCertFile` / `webKeyFile` | _(нет)_ | Сертификат TLS + ключ. Когда заданы оба, панель обслуживается по **HTTPS**. |
|
||||||
| `sessionMaxAge` | `360` | Время жизни сессии в **минутах** (по умолчанию 6 часов). |
|
| `sessionMaxAge` | `360` | Время жизни сессии в **минутах** (по умолчанию 6 часов). |
|
||||||
| `trustedProxyCIDRs` | `127.0.0.1/32,::1/128` | IP-адреса/CIDR, чьим переадресованным заголовкам (реальный IP клиента) можно доверять. |
|
| `trustedProxyCIDRs` | `127.0.0.1/32,::1/128` | IP-адреса/CIDR, чьим переадресованным заголовкам (реальный IP клиента) можно доверять. Пользовательское значение также управляет пересылаемыми хостом и схемой в ссылках подписки; добавьте прокси подписки или задайте `subURI`, чтобы переопределить эти ссылки. |
|
||||||
| `panelOutbound` | _(нет)_ | Маршрутизация собственного исходящего трафика панели (проверка обновлений, Telegram, запросы geo/подписок) через именованный исходящий канал Xray. |
|
| `panelOutbound` | _(нет)_ | Маршрутизация собственного исходящего трафика панели (проверка обновлений, Telegram, запросы geo/подписок) через именованный исходящий канал Xray. |
|
||||||
|
|
||||||
После изменения порта или базового пути URL панели становится
|
После изменения порта или базового пути URL панели становится
|
||||||
|
|||||||
@@ -123,6 +123,14 @@ vless://<uuid>@<server>:443?security=reality&pbk=<public-key>&sid=<short-id>&sni
|
|||||||
ключ.
|
ключ.
|
||||||
- **Неправильный поток.** Для REALITY + XTLS-Vision нужен `flow = xtls-rprx-vision`
|
- **Неправильный поток.** Для REALITY + XTLS-Vision нужен `flow = xtls-rprx-vision`
|
||||||
как в записи клиента входящего подключения, так и в ссылке для подключения.
|
как в записи клиента входящего подключения, так и в ссылке для подключения.
|
||||||
|
- **Старые ядра клиентов отклоняются по умолчанию.** Пустое поле
|
||||||
|
**Мин. версия клиента** не означает «без ограничений»: Xray-core использует
|
||||||
|
встроенный минимум используемой сборки ядра (26.3.27 в текущих релизах),
|
||||||
|
который поддерживает свежесть
|
||||||
|
TLS-отпечатков клиентов, поэтому сторонние ядра, такие как Mihomo и sing-box,
|
||||||
|
не проходят проверку REALITY даже при корректной конфигурации — клиенты видят
|
||||||
|
таймауты, а подключаются только приложения на базе Xray-core. Ставьте `1.0.0`,
|
||||||
|
только если они вам необходимы; это также допустит устаревшие отпечатки.
|
||||||
|
|
||||||
</Callout>
|
</Callout>
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,9 @@ icon: ArrowDownToLine
|
|||||||
|
|
||||||
可选地为入站设置总流量上限和到期日期,并选择一个周期性的**流量重置**计划:
|
可选地为入站设置总流量上限和到期日期,并选择一个周期性的**流量重置**计划:
|
||||||
`never`(默认)、`hourly`、`daily`、`weekly` 或 `monthly`。
|
`never`(默认)、`hourly`、`daily`、`weekly` 或 `monthly`。
|
||||||
|
|
||||||
|
选择 `monthly` 时,可以指定每月 1 至 31 日重置。如果当月没有指定日期,
|
||||||
|
则在该月最后一天重置。
|
||||||
</Step>
|
</Step>
|
||||||
|
|
||||||
</Steps>
|
</Steps>
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ icon: SlidersHorizontal
|
|||||||
| `webBasePath` | `/` | 面板对外提供服务所使用的 URL 路径(始终规范化为 `/…/`)。 |
|
| `webBasePath` | `/` | 面板对外提供服务所使用的 URL 路径(始终规范化为 `/…/`)。 |
|
||||||
| `webCertFile` / `webKeyFile` | _(无)_ | TLS 证书 + 密钥。两者都设置后,面板将以 **HTTPS** 提供服务。 |
|
| `webCertFile` / `webKeyFile` | _(无)_ | TLS 证书 + 密钥。两者都设置后,面板将以 **HTTPS** 提供服务。 |
|
||||||
| `sessionMaxAge` | `360` | 会话有效期,单位为**分钟**(默认 6 小时)。 |
|
| `sessionMaxAge` | `360` | 会话有效期,单位为**分钟**(默认 6 小时)。 |
|
||||||
| `trustedProxyCIDRs` | `127.0.0.1/32,::1/128` | 其转发头(真实客户端 IP)受信任的 IP/CIDR。 |
|
| `trustedProxyCIDRs` | `127.0.0.1/32,::1/128` | 其转发头(真实客户端 IP)受信任的 IP/CIDR。自定义值还会控制订阅链接中转发的主机和协议;请将订阅代理加入列表,或设置 `subURI` 覆盖这些链接。 |
|
||||||
| `panelOutbound` | _(无)_ | 通过一个命名的 Xray 出站来路由面板自身的出口流量(更新检查、Telegram、地理/订阅拉取)。 |
|
| `panelOutbound` | _(无)_ | 通过一个命名的 Xray 出站来路由面板自身的出口流量(更新检查、Telegram、地理/订阅拉取)。 |
|
||||||
|
|
||||||
更改端口或基础路径后,面板 URL 将变为
|
更改端口或基础路径后,面板 URL 将变为
|
||||||
|
|||||||
@@ -105,6 +105,7 @@ vless://<uuid>@<server>:443?security=reality&pbk=<public-key>&sid=<short-id>&sni
|
|||||||
- **SNI 不匹配。** SNI / server names 必须与目标站点的真实证书匹配,否则握手会暴露伪装。
|
- **SNI 不匹配。** SNI / server names 必须与目标站点的真实证书匹配,否则握手会暴露伪装。
|
||||||
- **私钥泄露。** 永远只把**公钥**分发给客户端。
|
- **私钥泄露。** 永远只把**公钥**分发给客户端。
|
||||||
- **流控设置错误。** REALITY + XTLS-Vision 要求在入站的客户端条目和分享链接上都设置 `flow = xtls-rprx-vision`。
|
- **流控设置错误。** REALITY + XTLS-Vision 要求在入站的客户端条目和分享链接上都设置 `flow = xtls-rprx-vision`。
|
||||||
|
- **旧客户端内核默认被拒。** **最小客户端版本**留空并不是“不限制”:Xray-core 会退回到所运行内核版本的内置最低值(当前版本为 26.3.27)以保证客户端 TLS 指纹的新鲜度,因此 Mihomo、sing-box 等第三方内核即使配置完全正确也会导致 REALITY 验证失败——表现为客户端超时,只有基于 Xray-core 的应用能连上。只有在必须支持它们时才填 `1.0.0`;这同时也会放行过时的指纹。
|
||||||
|
|
||||||
</Callout>
|
</Callout>
|
||||||
|
|
||||||
|
|||||||
@@ -22,23 +22,60 @@ 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 }}`):
|
When rendering the template, the following variables are injected into the template context (`{{ .variable }}`):
|
||||||
|
|
||||||
* `{{ .sId }}`: Subscription ID (UUID).
|
- `{{ .sId }}`: Subscription ID (UUID).
|
||||||
* `{{ .enabled }}`: Whether the subscription/client is enabled (boolean).
|
- `{{ .enabled }}`: Whether the subscription/client is enabled (boolean).
|
||||||
* `{{ .download }}`: Formatted download traffic (e.g. "2.5 GB").
|
- `{{ .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.
|
||||||
* `{{ .upload }}`: Formatted upload traffic.
|
- `{{ .download }}`: Formatted download traffic (e.g. "2.5 GB").
|
||||||
* `{{ .total }}`: Formatted total traffic limit.
|
- `{{ .upload }}`: Formatted upload traffic.
|
||||||
* `{{ .used }}`: Formatted used traffic (download + upload).
|
- `{{ .total }}`: Formatted total traffic limit.
|
||||||
* `{{ .remained }}`: Formatted remaining traffic.
|
- `{{ .used }}`: Formatted used traffic (download + upload).
|
||||||
* `{{ .expire }}`: Expiration time as an int64 Unix timestamp in **seconds** (`0` means never). Multiply by 1000 for a JavaScript `Date`.
|
- `{{ .remained }}`: Formatted remaining traffic.
|
||||||
* `{{ .lastOnline }}`: Last online time as an int64 Unix timestamp in **milliseconds** (`0` means never seen).
|
- `{{ .expire }}`: Expiration time as an int64 Unix timestamp in **seconds** (`0` means never). Multiply by 1000 for a JavaScript `Date`.
|
||||||
* `{{ .downloadByte }}`: Download traffic in exact bytes (int64).
|
- `{{ .lastOnline }}`: Last online time as an int64 Unix timestamp in **milliseconds** (`0` means never seen).
|
||||||
* `{{ .uploadByte }}`: Upload traffic in exact bytes (int64).
|
- `{{ .downloadByte }}`: Download traffic in exact bytes (int64).
|
||||||
* `{{ .totalByte }}`: Total traffic limit in exact bytes (int64).
|
- `{{ .uploadByte }}`: Upload traffic in exact bytes (int64).
|
||||||
* `{{ .subUrl }}`: The URL of the subscription page.
|
- `{{ .totalByte }}`: Total traffic limit in exact bytes (int64).
|
||||||
* `{{ .subJsonUrl }}`: The URL for the JSON configuration of the subscription.
|
- `{{ .subUrl }}`: The URL of the subscription page.
|
||||||
* `{{ .subClashUrl }}`: The URL for the Clash/Mihomo configuration.
|
- `{{ .subJsonUrl }}`: The URL for the JSON configuration of the subscription.
|
||||||
* `{{ .subTitle }}`: The subscription title configured in the panel (Subscription → Information). Useful for page branding/headings. May be empty.
|
- `{{ .subClashUrl }}`: The URL for the Clash/Mihomo configuration.
|
||||||
* `{{ .subSupportUrl }}`: The support URL configured in the panel. Useful for a "Contact support" link. May be empty.
|
- `{{ .subTitle }}`: The subscription title configured in the panel (Subscription → Information). Useful for page branding/headings. May be empty.
|
||||||
* `{{ .links }}`: A list (slice) of string configurations (VMess, VLESS, etc. URLs). You can loop through them using `{{ range .links }} ... {{ end }}`.
|
- `{{ .subSupportUrl }}`: The support URL configured in the panel. Useful for a "Contact support" link. May be empty.
|
||||||
* `{{ .emails }}`: A list (slice) of emails related to the subscription.
|
- `{{ .links }}`: A list (slice) of string configurations (VMess, VLESS, etc. URLs). You can loop through them using `{{ range .links }} ... {{ end }}`.
|
||||||
* `{{ .datepicker }}`: Current calendar format used by the panel (e.g. "gregorian" or "jalali").
|
- `{{ .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`)
|
||||||
|
|
||||||
|
Every subscription URL also answers `GET <sub URL>?format=info` with the same view-model as JSON —
|
||||||
|
minus `links`, and with `emails` deduplicated — so a template can poll it and update usage or
|
||||||
|
online status live without reloading the page:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"sId": "…",
|
||||||
|
"enabled": true,
|
||||||
|
"isOnline": true,
|
||||||
|
"used": "1.2 GB",
|
||||||
|
"remained": "8.8 GB",
|
||||||
|
"expire": 0,
|
||||||
|
"lastOnline": 1735680000000,
|
||||||
|
"…": "…"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Example polling snippet for a template:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<span id="status"></span>
|
||||||
|
<script>
|
||||||
|
async function refreshStatus() {
|
||||||
|
const res = await fetch(window.location.pathname + '?format=info');
|
||||||
|
if (!res.ok) return;
|
||||||
|
const info = await res.json();
|
||||||
|
document.getElementById('status').textContent = info.isOnline ? 'Online' : 'Offline';
|
||||||
|
}
|
||||||
|
refreshStatus();
|
||||||
|
setInterval(refreshStatus, 10000);
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|||||||
@@ -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 { Heart } from 'lucide-react';
|
||||||
import { Logo } from '@/components/logo';
|
import { Logo } from '@/components/logo';
|
||||||
import { TelegramIcon } from '@/components/icons';
|
import { TelegramIcon } from '@/components/icons';
|
||||||
import { appName, productRepoUrl, telegramChannel, telegramChannelUrl, donateUrl } from './shared';
|
import { DocsThemeSwitch } from '@/components/theme-switch';
|
||||||
|
import {
|
||||||
|
appName,
|
||||||
|
productRepoUrl,
|
||||||
|
telegramChannel,
|
||||||
|
telegramChannelUrl,
|
||||||
|
donateUrl,
|
||||||
|
siteUrl,
|
||||||
|
} from './shared';
|
||||||
import { getSiteMessages } from './site-i18n';
|
import { getSiteMessages } from './site-i18n';
|
||||||
|
|
||||||
// Build locale-aware shared layout options. With `hideLocale: 'default-locale'`,
|
// Build locale-aware shared layout options. With `hideLocale: 'default-locale'`,
|
||||||
@@ -12,6 +20,9 @@ export function baseOptions(lang: string): BaseLayoutProps {
|
|||||||
const m = getSiteMessages(lang);
|
const m = getSiteMessages(lang);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
slots: {
|
||||||
|
themeSwitch: DocsThemeSwitch,
|
||||||
|
},
|
||||||
nav: {
|
nav: {
|
||||||
title: (
|
title: (
|
||||||
<span className="inline-flex items-center gap-2 font-semibold">
|
<span className="inline-flex items-center gap-2 font-semibold">
|
||||||
@@ -28,6 +39,12 @@ export function baseOptions(lang: string): BaseLayoutProps {
|
|||||||
url: `${prefix}/docs`,
|
url: `${prefix}/docs`,
|
||||||
active: 'nested-url',
|
active: 'nested-url',
|
||||||
},
|
},
|
||||||
|
// Live component workbench built from frontend/ and published alongside the docs.
|
||||||
|
{
|
||||||
|
text: 'Storybook',
|
||||||
|
url: `${siteUrl}/storybook/`,
|
||||||
|
external: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
type: 'icon',
|
type: 'icon',
|
||||||
label: `Telegram channel (@${telegramChannel})`,
|
label: `Telegram channel (@${telegramChannel})`,
|
||||||
|
|||||||
@@ -222,7 +222,8 @@ const zh: SiteMessages = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '自托管且可脚本化',
|
title: '自托管且可脚本化',
|
||||||
description: '单个 Go 二进制文件或 Docker 镜像、SQLite/PostgreSQL 后端,以及用于自动化的完整 REST API。',
|
description:
|
||||||
|
'单个 Go 二进制文件或 Docker 镜像、SQLite/PostgreSQL 后端,以及用于自动化的完整 REST API。',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
licenseBefore: '基于 ',
|
licenseBefore: '基于 ',
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ const base = {
|
|||||||
describe('buildCurl', () => {
|
describe('buildCurl', () => {
|
||||||
it('GET emits the Bearer header, a single-quoted URL, and no body flag', () => {
|
it('GET emits the Bearer header, a single-quoted URL, and no body flag', () => {
|
||||||
const cmd = buildCurl({ ...base, method: 'GET' });
|
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("-H 'Authorization: Bearer TKN'");
|
||||||
expect(cmd).toContain("'https://panel.example.com:2053/panel/api/inbounds/list'");
|
expect(cmd).toContain("'https://panel.example.com:2053/panel/api/inbounds/list'");
|
||||||
expect(cmd).not.toContain('--data');
|
expect(cmd).not.toContain('--data');
|
||||||
@@ -39,14 +39,23 @@ describe('buildCurl', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('POST with a body emits --data and a JSON content type', () => {
|
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('-X POST');
|
||||||
expect(cmd).toContain("--data '{\"up\":0}'");
|
expect(cmd).toContain('--data \'{"up":0}\'');
|
||||||
expect(cmd).toContain("Content-Type: application/json");
|
expect(cmd).toContain('Content-Type: application/json');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('POST without a body omits --data', () => {
|
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');
|
expect(cmd).not.toContain('--data');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -60,7 +69,12 @@ describe('buildFetchSnippet', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('POST with a body includes a JSON.stringify body', () => {
|
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("method: 'POST'");
|
||||||
expect(snip).toContain('body: JSON.stringify(');
|
expect(snip).toContain('body: JSON.stringify(');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -160,7 +160,12 @@ describe('buildOutbound — wireguard & warp', () => {
|
|||||||
const ob = buildOutbound({
|
const ob = buildOutbound({
|
||||||
kind: 'wireguard',
|
kind: 'wireguard',
|
||||||
tag: 'wg',
|
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>;
|
const s = ob.settings as Record<string, unknown>;
|
||||||
expect(s.secretKey).toBe('sk');
|
expect(s.secretKey).toBe('sk');
|
||||||
|
|||||||
@@ -162,7 +162,11 @@ function buildSettings(o: OutboundInput): Record<string, unknown> {
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
case 'trojan':
|
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':
|
case 'shadowsocks':
|
||||||
return {
|
return {
|
||||||
servers: [
|
servers: [
|
||||||
|
|||||||
@@ -18,7 +18,12 @@ describe('buildBalancer', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('includes fallbackTag when set', () => {
|
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');
|
expect(b.fallbackTag).toBe('direct');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -121,7 +121,10 @@ export function buildRouting(input: RoutingInput): Record<string, unknown> {
|
|||||||
if (input.observatory) {
|
if (input.observatory) {
|
||||||
Object.assign(out, buildObservatory(input.observatory));
|
Object.assign(out, buildObservatory(input.observatory));
|
||||||
} else if (input.balancers.some((b) => b.strategy === 'leastLoad')) {
|
} 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')) {
|
} else if (input.balancers.some((b) => b.strategy === 'leastPing')) {
|
||||||
Object.assign(
|
Object.assign(
|
||||||
out,
|
out,
|
||||||
|
|||||||
@@ -214,12 +214,20 @@ function proxyOutbound(c: SubClient): Record<string, unknown> {
|
|||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
case 'trojan':
|
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;
|
break;
|
||||||
case 'ss':
|
case 'ss':
|
||||||
settings = {
|
settings = {
|
||||||
servers: [
|
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;
|
break;
|
||||||
|
|||||||
@@ -36,7 +36,10 @@ describe('parseAdminIds', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('accepts negative group ids and captures invalid entries', () => {
|
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', () => {
|
it('returns empty for blank input', () => {
|
||||||
@@ -78,9 +81,9 @@ describe('telegramApiBase', () => {
|
|||||||
|
|
||||||
describe('renderMessageTemplate', () => {
|
describe('renderMessageTemplate', () => {
|
||||||
it('substitutes known variables', () => {
|
it('substitutes known variables', () => {
|
||||||
expect(renderMessageTemplate('Host {{host}} up {{uptime}}', { host: 'srv', uptime: '3d' })).toBe(
|
expect(
|
||||||
'Host srv up 3d',
|
renderMessageTemplate('Host {{host}} up {{uptime}}', { host: 'srv', uptime: '3d' }),
|
||||||
);
|
).toBe('Host srv up 3d');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('leaves unknown variables literal', () => {
|
it('leaves unknown variables literal', () => {
|
||||||
@@ -90,7 +93,11 @@ describe('renderMessageTemplate', () => {
|
|||||||
|
|
||||||
describe('buildBotConfigSummary', () => {
|
describe('buildBotConfigSummary', () => {
|
||||||
it('emits the panel settings keys with admin ids joined', () => {
|
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.tgBotEnable).toBe(true);
|
||||||
expect(s.tgBotToken).toBe(VALID_TOKEN);
|
expect(s.tgBotToken).toBe(VALID_TOKEN);
|
||||||
expect(s.tgBotChatId).toBe('111,222');
|
expect(s.tgBotChatId).toBe('111,222');
|
||||||
|
|||||||
@@ -43,7 +43,10 @@ export function validateBotToken(token: string): TokenValidation {
|
|||||||
export function parseAdminIds(raw: string): AdminIdsResult {
|
export function parseAdminIds(raw: string): AdminIdsResult {
|
||||||
const ids: number[] = [];
|
const ids: number[] = [];
|
||||||
const invalid: string[] = [];
|
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.
|
// Telegram chat ids are integers; group/channel ids are negative.
|
||||||
if (/^-?\d+$/.test(part)) ids.push(Number(part));
|
if (/^-?\d+$/.test(part)) ids.push(Number(part));
|
||||||
else invalid.push(part);
|
else invalid.push(part);
|
||||||
|
|||||||
+26
-27
@@ -9,44 +9,43 @@
|
|||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"postinstall": "fumadocs-mdx",
|
"postinstall": "fumadocs-mdx",
|
||||||
"gen:api": "node --experimental-strip-types scripts/gen-openapi.ts",
|
"gen:api": "node scripts/gen-openapi.ts",
|
||||||
"typecheck": "fumadocs-mdx && next typegen && tsc --noEmit",
|
"typecheck": "fumadocs-mdx && next typegen && tsc --noEmit",
|
||||||
"lint": "eslint .",
|
"lint": "oxlint .",
|
||||||
"format": "prettier --write .",
|
"format": "oxfmt .",
|
||||||
"format:check": "prettier --check .",
|
"format:check": "oxfmt --check .",
|
||||||
"test": "vitest run",
|
"test": "vitest run",
|
||||||
"test:watch": "vitest"
|
"test:watch": "vitest"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@orama/orama": "^3.1.18",
|
"fumadocs-core": "^16.14.5",
|
||||||
"fumadocs-core": "^16.10.5",
|
"fumadocs-docgen": "^3.1.0",
|
||||||
"fumadocs-docgen": "^3.0.10",
|
"fumadocs-mdx": "^15.3.0",
|
||||||
"fumadocs-mdx": "^15.0.12",
|
"fumadocs-openapi": "^11.2.4",
|
||||||
"fumadocs-openapi": "^11.0.5",
|
"fumadocs-ui": "^16.14.5",
|
||||||
"fumadocs-ui": "^16.10.5",
|
"lucide-react": "^1.33.0",
|
||||||
"lucide-react": "^1.21.0",
|
"mermaid": "^11.17.0",
|
||||||
"mermaid": "^11.16.0",
|
"next": "16.3.1",
|
||||||
"next": "16.2.9",
|
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"react": "^19.2.7",
|
"react": "^19.2.8",
|
||||||
"react-dom": "^19.2.7",
|
"react-dom": "^19.2.8",
|
||||||
"react-qr-code": "^2.2.0",
|
"react-qr-code": "^2.2.0",
|
||||||
"tailwind-merge": "^3.6.0",
|
"tailwind-merge": "^3.6.0",
|
||||||
|
"zbsearch": "4.0.0",
|
||||||
"zod": "^4.4.3"
|
"zod": "^4.4.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/postcss": "^4.3.1",
|
"@tailwindcss/postcss": "^4.3.3",
|
||||||
"@types/mdx": "^2.0.14",
|
"@types/mdx": "^2.0.14",
|
||||||
"@types/node": "^26.0.1",
|
"@types/node": "^26.2.0",
|
||||||
"@types/react": "^19.2.17",
|
"@types/react": "^19.2.18",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.4",
|
||||||
"eslint": "^9.39.4",
|
"oxfmt": "0.64.0",
|
||||||
"eslint-config-next": "16.2.9",
|
"oxlint": "1.79.0",
|
||||||
"postcss": "^8.5.15",
|
"postcss": "^8.5.26",
|
||||||
"prettier": "^3.8.4",
|
"tailwindcss": "^4.3.3",
|
||||||
"tailwindcss": "^4.3.1",
|
"typescript": "7.0.2",
|
||||||
"typescript": "^6.0.3",
|
"vitest": "^4.1.11"
|
||||||
"vitest": "^4.1.9"
|
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@11.9.0"
|
"packageManager": "pnpm@11.22.0+sha512.1ff870c4c6133dfd88fb2afc46dd13d47f09c9794b438c6fdb47ca98caf3bc16381ee0be93a091b8e3824cf01f889f46d7d9e20910fb0be1ab0fb5baa80dd621"
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+2924
-4655
File diff suppressed because it is too large
Load Diff
@@ -6,6 +6,10 @@ allowBuilds:
|
|||||||
# release — fixes GHSA-qx2v-qp2m-jg93 / CVE-2026-41305 (vulnerable < 8.5.10).
|
# release — fixes GHSA-qx2v-qp2m-jg93 / CVE-2026-41305 (vulnerable < 8.5.10).
|
||||||
overrides:
|
overrides:
|
||||||
'postcss@<8.5.10': '^8.5.15'
|
'postcss@<8.5.10': '^8.5.15'
|
||||||
|
'sharp@<0.35.0': '^0.35.3'
|
||||||
minimumReleaseAgeExclude:
|
minimumReleaseAgeExclude:
|
||||||
- '@mermaid-js/parser@1.2.0'
|
- '@mermaid-js/parser@1.2.1'
|
||||||
- mermaid@11.16.0
|
- mermaid@11.17.0
|
||||||
|
- lucide-react@1.33.0
|
||||||
|
- postcss@8.5.26
|
||||||
|
- fumadocs-mdx@15.3.0
|
||||||
|
|||||||
+162
-6
@@ -411,6 +411,9 @@
|
|||||||
"maximum": 65535,
|
"maximum": 65535,
|
||||||
"minimum": 1,
|
"minimum": 1,
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"subShowIdentityOnAllLinks": {
|
||||||
|
"type": "boolean"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
@@ -479,6 +482,7 @@
|
|||||||
"subPort",
|
"subPort",
|
||||||
"subProfileUrl",
|
"subProfileUrl",
|
||||||
"subRoutingRules",
|
"subRoutingRules",
|
||||||
|
"subShowIdentityOnAllLinks",
|
||||||
"subSupportUrl",
|
"subSupportUrl",
|
||||||
"subThemeDir",
|
"subThemeDir",
|
||||||
"subTitle",
|
"subTitle",
|
||||||
@@ -916,6 +920,9 @@
|
|||||||
"maximum": 65535,
|
"maximum": 65535,
|
||||||
"minimum": 1,
|
"minimum": 1,
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"subShowIdentityOnAllLinks": {
|
||||||
|
"type": "boolean"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
@@ -991,6 +998,7 @@
|
|||||||
"subPort",
|
"subPort",
|
||||||
"subProfileUrl",
|
"subProfileUrl",
|
||||||
"subRoutingRules",
|
"subRoutingRules",
|
||||||
|
"subShowIdentityOnAllLinks",
|
||||||
"subSupportUrl",
|
"subSupportUrl",
|
||||||
"subThemeDir",
|
"subThemeDir",
|
||||||
"subTitle",
|
"subTitle",
|
||||||
@@ -1025,17 +1033,25 @@
|
|||||||
"ApiToken": {
|
"ApiToken": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"createdAt": {
|
"createdAt": {
|
||||||
|
"format": "int64",
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
"enabled": {
|
"enabled": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
|
"expiresAt": {
|
||||||
|
"format": "int64",
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
"id": {
|
"id": {
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
"name": {
|
"name": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"scope": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"token": {
|
"token": {
|
||||||
"description": "SHA-256 hash; the plaintext is shown only once at creation",
|
"description": "SHA-256 hash; the plaintext is shown only once at creation",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
@@ -1044,8 +1060,10 @@
|
|||||||
"required": [
|
"required": [
|
||||||
"createdAt",
|
"createdAt",
|
||||||
"enabled",
|
"enabled",
|
||||||
|
"expiresAt",
|
||||||
"id",
|
"id",
|
||||||
"name",
|
"name",
|
||||||
|
"scope",
|
||||||
"token"
|
"token"
|
||||||
],
|
],
|
||||||
"type": "object"
|
"type": "object"
|
||||||
@@ -1054,12 +1072,18 @@
|
|||||||
"properties": {
|
"properties": {
|
||||||
"createdAt": {
|
"createdAt": {
|
||||||
"example": 1736000000,
|
"example": 1736000000,
|
||||||
|
"format": "int64",
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
"enabled": {
|
"enabled": {
|
||||||
"example": true,
|
"example": true,
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
|
"expiresAt": {
|
||||||
|
"example": 0,
|
||||||
|
"format": "int64",
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
"id": {
|
"id": {
|
||||||
"example": 2,
|
"example": 2,
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
@@ -1068,6 +1092,10 @@
|
|||||||
"example": "central-panel-a",
|
"example": "central-panel-a",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"scope": {
|
||||||
|
"example": "admin",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"token": {
|
"token": {
|
||||||
"example": "new-token-string",
|
"example": "new-token-string",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
@@ -1076,8 +1104,10 @@
|
|||||||
"required": [
|
"required": [
|
||||||
"createdAt",
|
"createdAt",
|
||||||
"enabled",
|
"enabled",
|
||||||
|
"expiresAt",
|
||||||
"id",
|
"id",
|
||||||
"name"
|
"name",
|
||||||
|
"scope"
|
||||||
],
|
],
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
@@ -8809,7 +8839,7 @@
|
|||||||
"tags": [
|
"tags": [
|
||||||
"API Tokens"
|
"API Tokens"
|
||||||
],
|
],
|
||||||
"summary": "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.",
|
"summary": "Mint a scoped API token. The server-generated plaintext is returned only once and stored as a hash.",
|
||||||
"operationId": "post_panel_api_setting_apiTokens_create",
|
"operationId": "post_panel_api_setting_apiTokens_create",
|
||||||
"requestBody": {
|
"requestBody": {
|
||||||
"required": true,
|
"required": true,
|
||||||
@@ -8821,14 +8851,26 @@
|
|||||||
"name": {
|
"name": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Human-readable label, e.g. \"central-panel-a\"."
|
"description": "Human-readable label, e.g. \"central-panel-a\"."
|
||||||
|
},
|
||||||
|
"scope": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "admin (default), monitor, or node-sync."
|
||||||
|
},
|
||||||
|
"expiresAt": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Future Unix milliseconds, or 0 for no expiry."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
"name"
|
"name",
|
||||||
|
"scope",
|
||||||
|
"expiresAt"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"example": {
|
"example": {
|
||||||
"name": "central-panel-a"
|
"name": "central-panel-a",
|
||||||
|
"scope": "node-sync",
|
||||||
|
"expiresAt": 1798761600000
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -8857,8 +8899,10 @@
|
|||||||
"obj": {
|
"obj": {
|
||||||
"createdAt": 1736000000,
|
"createdAt": 1736000000,
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
|
"expiresAt": 0,
|
||||||
"id": 2,
|
"id": 2,
|
||||||
"name": "central-panel-a",
|
"name": "central-panel-a",
|
||||||
|
"scope": "admin",
|
||||||
"token": "new-token-string"
|
"token": "new-token-string"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -8908,6 +8952,28 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"requestBody": {
|
||||||
|
"required": true,
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"expectedScope": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Stored scope expected by the operator."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"expectedScope"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"example": {
|
||||||
|
"expectedScope": "node-sync"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "Successful response",
|
"description": "Successful response",
|
||||||
@@ -8962,14 +9028,20 @@
|
|||||||
"enabled": {
|
"enabled": {
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"description": "New enabled state."
|
"description": "New enabled state."
|
||||||
|
},
|
||||||
|
"expectedScope": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Stored scope expected by the operator."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
"enabled"
|
"enabled",
|
||||||
|
"expectedScope"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"example": {
|
"example": {
|
||||||
"enabled": false
|
"enabled": false,
|
||||||
|
"expectedScope": "node-sync"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -10098,6 +10170,90 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"/panel/api/nodes/mtls/reloadClient": {
|
||||||
|
"post": {
|
||||||
|
"tags": [
|
||||||
|
"Nodes"
|
||||||
|
],
|
||||||
|
"summary": "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.",
|
||||||
|
"operationId": "post_panel_api_nodes_mtls_reloadClient",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Successful response",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"success": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"msg": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"obj": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/panel/api/inbounds/{id}/subSortIndex": {
|
||||||
|
"post": {
|
||||||
|
"tags": [
|
||||||
|
"Inbounds"
|
||||||
|
],
|
||||||
|
"summary": "Set only the subscription sort order. Reads the stored inbound, so a reorder cannot carry a stale client list over a concurrent edit.",
|
||||||
|
"operationId": "post_panel_api_inbounds_id_subSortIndex",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true,
|
||||||
|
"description": "Inbound ID.",
|
||||||
|
"schema": {
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"requestBody": {
|
||||||
|
"required": true,
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
"example": {
|
||||||
|
"subSortIndex": 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Successful response",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"success": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"msg": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"obj": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -15,7 +15,7 @@ Open an inbound → **Transport / Stream Settings** → enable **Sockopt** → u
|
|||||||
**Real client IP** preset selector:
|
**Real client IP** preset selector:
|
||||||
|
|
||||||
| Preset | What it does | Use for |
|
| Preset | What it does | Use for |
|
||||||
|---|---|---|
|
| ------------------------------- | ----------------------------------------------------------- | ----------------------------------------------------------------------- |
|
||||||
| **Off / direct** | Clears both fields. | Inbound reachable directly by clients. |
|
| **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). |
|
| **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**. |
|
| **L4 relay / Spectrum (PROXY)** | Sets `acceptProxyProtocol = true`. | An L4 tunnel/relay in front, or Cloudflare **Spectrum**. |
|
||||||
@@ -66,7 +66,7 @@ and XHTTP; **not** on mKCP. The front must be configured to send the header, e.g
|
|||||||
## Transport support matrix
|
## Transport support matrix
|
||||||
|
|
||||||
| Mechanism | TCP/RAW | mKCP | WebSocket | gRPC | HTTPUpgrade | XHTTP |
|
| Mechanism | TCP/RAW | mKCP | WebSocket | gRPC | HTTPUpgrade | XHTTP |
|
||||||
|---|:--:|:--:|:--:|:--:|:--:|:--:|
|
| ------------------------------- | :-----: | :--: | :-------: | :--: | :---------: | :---: |
|
||||||
| `trustedXForwardedFor` (header) | – | – | ✅ | – | ✅ | ✅ |
|
| `trustedXForwardedFor` (header) | – | – | ✅ | – | ✅ | ✅ |
|
||||||
| `acceptProxyProtocol` (PROXY) | ✅ | – | ✅ | ✅ | ✅ | ✅ |
|
| `acceptProxyProtocol` (PROXY) | ✅ | – | ✅ | ✅ | ✅ | ✅ |
|
||||||
|
|
||||||
@@ -74,7 +74,7 @@ The form shows a warning when you select a preset that the current transport can
|
|||||||
|
|
||||||
> **Use one, not both.** `acceptProxyProtocol` and `trustedXForwardedFor` are independent — the
|
> **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
|
> 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
|
> `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
|
> mutually exclusive and clear the other field for you; only mix them by hand if you know your
|
||||||
> upstream chain needs it.
|
> 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -6,7 +6,11 @@ const config: StorybookConfig = {
|
|||||||
options: {},
|
options: {},
|
||||||
},
|
},
|
||||||
stories: ['../src/**/*.stories.@(ts|tsx)'],
|
stories: ['../src/**/*.stories.@(ts|tsx)'],
|
||||||
addons: ['@storybook/addon-docs', '@storybook/addon-a11y'],
|
addons: [
|
||||||
|
'@storybook/addon-docs',
|
||||||
|
'@storybook/addon-a11y',
|
||||||
|
'@storybook/addon-vitest'
|
||||||
|
],
|
||||||
viteFinal: (viteConfig) => {
|
viteFinal: (viteConfig) => {
|
||||||
if (viteConfig.build) {
|
if (viteConfig.build) {
|
||||||
viteConfig.build.outDir = undefined;
|
viteConfig.build.outDir = undefined;
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<script>
|
||||||
|
if (localStorage.getItem('dark-mode') === null) localStorage.setItem('dark-mode', 'false');
|
||||||
|
if (localStorage.getItem('isUltraDarkThemeEnabled') === null) {
|
||||||
|
localStorage.setItem('isUltraDarkThemeEnabled', 'false');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
import { useEffect } from 'react';
|
import { useLayoutEffect } from 'react';
|
||||||
import type { Decorator, Preview } from '@storybook/react-vite';
|
import type { Decorator, Preview } from '@storybook/react-vite';
|
||||||
import { ConfigProvider, theme as antdTheme } from 'antd';
|
import { ConfigProvider } from 'antd';
|
||||||
import i18next from 'i18next';
|
import i18next from 'i18next';
|
||||||
import { initReactI18next } from 'react-i18next';
|
import { initReactI18next } from 'react-i18next';
|
||||||
|
|
||||||
|
import { buildAntdThemeConfig } from '@/hooks/useTheme';
|
||||||
import enUS from '../../internal/web/translation/en-US.json';
|
import enUS from '../../internal/web/translation/en-US.json';
|
||||||
|
|
||||||
if (!i18next.isInitialized) {
|
if (!i18next.isInitialized) {
|
||||||
@@ -16,13 +17,15 @@ if (!i18next.isInitialized) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const withTheme: Decorator = (Story, context) => {
|
export const withTheme: Decorator = (Story, context) => {
|
||||||
const dark = context.globals.theme === 'dark';
|
const dark = context.globals.theme === 'dark';
|
||||||
useEffect(() => {
|
useLayoutEffect(() => {
|
||||||
document.documentElement.setAttribute('data-theme', dark ? 'dark' : 'light');
|
document.body.classList.remove('dark', 'light');
|
||||||
|
document.body.classList.add(dark ? 'dark' : 'light');
|
||||||
|
document.documentElement.removeAttribute('data-theme');
|
||||||
}, [dark]);
|
}, [dark]);
|
||||||
return (
|
return (
|
||||||
<ConfigProvider theme={{ algorithm: dark ? antdTheme.darkAlgorithm : antdTheme.defaultAlgorithm }}>
|
<ConfigProvider theme={buildAntdThemeConfig(dark, false)}>
|
||||||
<div style={{ padding: 24, minWidth: 320 }}>
|
<div style={{ padding: 24, minWidth: 320 }}>
|
||||||
<Story />
|
<Story />
|
||||||
</div>
|
</div>
|
||||||
@@ -49,11 +52,16 @@ const preview: Preview = {
|
|||||||
},
|
},
|
||||||
parameters: {
|
parameters: {
|
||||||
controls: {
|
controls: {
|
||||||
|
expanded: true,
|
||||||
|
sort: 'requiredFirst',
|
||||||
matchers: {
|
matchers: {
|
||||||
color: /(background|color)$/i,
|
color: /(background|color)$/i,
|
||||||
date: /Date$/i,
|
date: /Date$/i,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
a11y: {
|
||||||
|
test: 'error',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+7
-2
@@ -31,8 +31,9 @@ The `@` import alias maps to `src/`.
|
|||||||
Form *state* runs on React Hook Form (`src/components/form/rhf/`), not Ant
|
Form *state* runs on React Hook Form (`src/components/form/rhf/`), not Ant
|
||||||
Design's `Form` store.
|
Design's `Form` store.
|
||||||
- Function components + hooks only; no class components.
|
- Function components + hooks only; no class components.
|
||||||
- No `//` line comments in committed TS/TSX. HTML comments are fine.
|
- Comments in committed TS/TSX: 2 lines MAX per comment block, spent on the
|
||||||
- TS strict; `no-explicit-any` is an error. Build forms with `useZodForm` +
|
*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`);
|
`FormField` from `@/components/form/rhf` (wrap the tree in `FormProvider`);
|
||||||
validate through the `zodResolver` or per-field
|
validate through the `zodResolver` or per-field
|
||||||
`rules={{ validate: rhfZodValidate(Schema.shape.field) }}` — messages are Zod
|
`rules={{ validate: rhfZodValidate(Schema.shape.field) }}` — messages are Zod
|
||||||
@@ -63,5 +64,9 @@ Only standalone bundles (login/subpage) need a new `.html` + `src/entries/*` +
|
|||||||
- `npm run typecheck` / `npm run lint` / `npm run test` / `npm run build`.
|
- `npm run typecheck` / `npm run lint` / `npm run test` / `npm run build`.
|
||||||
- `npm run gen` = `gen:zod` (Go → `src/generated/`) + `gen:api`
|
- `npm run gen` = `gen:zod` (Go → `src/generated/`) + `gen:api`
|
||||||
(`build-openapi.mjs` → `public/openapi.json`).
|
(`build-openapi.mjs` → `public/openapi.json`).
|
||||||
|
- `npm run storybook` (workbench on :6006) / `npm run build-storybook` (CI
|
||||||
|
compile-checks every story). Reusable `src/components/` get a co-located
|
||||||
|
`<Component>.stories.tsx` with `tags: ['autodocs']`; document props via
|
||||||
|
`argTypes` / `parameters.docs` string metadata, never JSDoc.
|
||||||
- After `npm run build`, RESTART `go run .` (see the XUI_DEBUG gotcha in root
|
- After `npm run build`, RESTART `go run .` (see the XUI_DEBUG gotcha in root
|
||||||
CLAUDE.md) before checking the panel.
|
CLAUDE.md) before checking the panel.
|
||||||
|
|||||||
+61
-12
@@ -33,14 +33,19 @@ production-style links work without round-tripping through Go.
|
|||||||
| `npm run build` | Regenerates OpenAPI + Zod, then builds into `../internal/web/dist/` |
|
| `npm run build` | Regenerates OpenAPI + Zod, then builds into `../internal/web/dist/` |
|
||||||
| `npm run preview` | Serve the built bundle locally |
|
| `npm run preview` | Serve the built bundle locally |
|
||||||
| `npm run typecheck` | `tsc --noEmit` (strict, no emit) |
|
| `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` | Vitest single run (schema fixtures, link parsers, …) |
|
||||||
| `npm run test:watch` | Vitest watch mode |
|
| `npm run test:watch` | Vitest watch mode |
|
||||||
|
| `npm run storybook` | Storybook dev server on `:6006` (component workbench + autodocs) |
|
||||||
|
| `npm run build-storybook` | Static Storybook build — CI compile-checks every story |
|
||||||
| `npm run gen:api` | Build `public/openapi.json` from `pages/api-docs/endpoints.ts` |
|
| `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` |
|
| `npm run gen:zod` | Run the Go-side openapigen tool → `src/generated/{zod,types}.ts` |
|
||||||
|
|
||||||
CI runs `typecheck`, `lint`, `test`, and `build` on every PR
|
CI runs `typecheck`, `lint`, `format:check`, `test`, `build`, and
|
||||||
(see `../.github/workflows/ci.yml`).
|
`build-storybook` on every PR (see `../.github/workflows/ci.yml`).
|
||||||
|
|
||||||
### One-off: scan for deprecated APIs
|
### One-off: scan for deprecated APIs
|
||||||
|
|
||||||
@@ -49,12 +54,13 @@ with the JSDoc `@deprecated` tag (AntD prop renames, Zod renames,
|
|||||||
removed Web APIs, etc.):
|
removed Web APIs, etc.):
|
||||||
|
|
||||||
```sh
|
```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`
|
It is oxlint's type-aware mode (`oxlint-tsgolint`, which drives the
|
||||||
and is not wired into `npm run lint` because typed linting triples
|
TypeScript 7 `typescript-go` checker) narrowed to `no-deprecated`, and
|
||||||
the wall-clock time.
|
is not wired into `npm run lint` because typed linting needs a full
|
||||||
|
type-check pass.
|
||||||
|
|
||||||
## Production build
|
## Production build
|
||||||
|
|
||||||
@@ -68,17 +74,30 @@ react-query into separate vendor bundles to keep the per-page
|
|||||||
initial JS small. The Go binary embeds this directory at compile
|
initial JS small. The Go binary embeds this directory at compile
|
||||||
time and `internal/web/controller/dist.go` serves the per-page HTML.
|
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
|
## Layout
|
||||||
|
|
||||||
```
|
```
|
||||||
frontend/
|
frontend/
|
||||||
├── index.html, login.html, subpage.html # 3 Vite entries
|
├── index.html, login.html, subpage.html # 3 Vite entries
|
||||||
├── tsconfig.json
|
├── tsconfig.json
|
||||||
├── eslint.config.js
|
├── .oxlintrc.json # oxlint config (replaces the ESLint flat config)
|
||||||
├── eslint.deprecated.config.js # On-demand type-aware lint config that flags
|
├── .oxfmtrc.json # oxfmt config (Prettier-compatible settings)
|
||||||
│ # usages of APIs marked with JSDoc @deprecated
|
├── tools/oxlint/
|
||||||
|
│ └── input-number-guard.mjs # oxlint JS plugin: the #6121/#6127 cleared-
|
||||||
|
│ # InputNumber guard (oxlint has no
|
||||||
|
│ # no-restricted-syntax)
|
||||||
├── vitest.config.ts
|
├── vitest.config.ts
|
||||||
├── vite.config.js
|
├── vite.config.js
|
||||||
|
├── .storybook/ # Storybook config (main.ts, preview.tsx)
|
||||||
├── scripts/
|
├── scripts/
|
||||||
│ └── build-openapi.mjs # endpoints.ts → openapi.json
|
│ └── build-openapi.mjs # endpoints.ts → openapi.json
|
||||||
└── src/
|
└── src/
|
||||||
@@ -89,7 +108,7 @@ frontend/
|
|||||||
│ ├── index/, login/, inbounds/, clients/, xray/, nodes/,
|
│ ├── index/, login/, inbounds/, clients/, xray/, nodes/,
|
||||||
│ ├── settings/, api-docs/, sub/
|
│ ├── settings/, api-docs/, sub/
|
||||||
├── layouts/ # AdminLayout (sidebar + header + outlet)
|
├── layouts/ # AdminLayout (sidebar + header + outlet)
|
||||||
├── components/ # Cross-page React components
|
├── components/ # Cross-page React components (+ co-located *.stories.tsx)
|
||||||
├── hooks/ # useClients, useTheme, useWebSocket, …
|
├── hooks/ # useClients, useTheme, useWebSocket, …
|
||||||
├── api/ # fetch client + CSRF handling, TanStack Query bridge,
|
├── api/ # fetch client + CSRF handling, TanStack Query bridge,
|
||||||
│ # WebSocket client + queryClient.ts
|
│ # WebSocket client + queryClient.ts
|
||||||
@@ -143,7 +162,7 @@ Patterns:
|
|||||||
- Wire request: `Schema.parse(payload)` inside `mutationFn` — throws,
|
- Wire request: `Schema.parse(payload)` inside `mutationFn` — throws,
|
||||||
because a malformed payload here is always a developer bug
|
because a malformed payload here is always a developer bug
|
||||||
- **No `.loose()` or `[key: string]: any`** in production schemas.
|
- **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)
|
## Form pattern (Pattern A)
|
||||||
|
|
||||||
@@ -187,6 +206,36 @@ npx vitest run -u
|
|||||||
Fixtures live in `src/test/golden/fixtures/` and are auto-discovered
|
Fixtures live in `src/test/golden/fixtures/` and are auto-discovered
|
||||||
via `import.meta.glob`.
|
via `import.meta.glob`.
|
||||||
|
|
||||||
|
## Storybook
|
||||||
|
|
||||||
|
Reusable components in `src/components/` are developed and documented in
|
||||||
|
**Storybook** (`@storybook/react-vite`). It is a component workbench, not part
|
||||||
|
of the shipped panel — nothing here is embedded into the Go binary. The built
|
||||||
|
Storybook is published with the docs site at
|
||||||
|
[docs.sanaei.dev/storybook](https://docs.sanaei.dev/storybook/) by
|
||||||
|
`.github/workflows/docs-deploy.yml`.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm run storybook # dev server on http://localhost:6006
|
||||||
|
npm run build-storybook # static build; CI runs this to compile-check every story
|
||||||
|
```
|
||||||
|
|
||||||
|
Addons: `@storybook/addon-docs` renders an autodocs page per component,
|
||||||
|
`@storybook/addon-a11y` flags accessibility issues in the canvas, and
|
||||||
|
`@storybook/addon-vitest` runs every story as a headless-browser test under
|
||||||
|
`npm run test` (Playwright/Chromium — run `npx playwright install chromium` once
|
||||||
|
locally). The `.storybook/preview.tsx` decorator wraps every story in the AntD
|
||||||
|
`ConfigProvider` and adds a light/dark theme toggle to the toolbar.
|
||||||
|
|
||||||
|
Conventions for a story:
|
||||||
|
|
||||||
|
- Co-locate it with its component as `<Component>.stories.tsx`.
|
||||||
|
- Set `tags: ['autodocs']` so it gets a generated docs page.
|
||||||
|
- Document props via story metadata, not JSDoc (the repo bans `//` comments): a
|
||||||
|
component summary in `parameters.docs.description.component` and per-prop text
|
||||||
|
in `argTypes[prop].description`. `satisfies Meta<typeof Component>` keeps the
|
||||||
|
metadata type-checked.
|
||||||
|
|
||||||
## Adding a new page
|
## Adding a new page
|
||||||
|
|
||||||
Most new routes go inside the admin SPA (`index.html`) via
|
Most new routes go inside the admin SPA (`index.html`) via
|
||||||
|
|||||||
@@ -1,56 +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',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
@@ -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
+2744
-4162
File diff suppressed because it is too large
Load Diff
+49
-41
@@ -1,84 +1,88 @@
|
|||||||
{
|
{
|
||||||
"name": "3x-ui-frontend",
|
"name": "3x-ui-frontend",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.4.3",
|
"version": "0.6.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "3x-ui panel frontend (React 19 + Ant Design 6 + Vite 8).",
|
"description": "3x-ui panel frontend (React 19 + Ant Design 6 + Vite 8).",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=22.0.0",
|
"node": ">=24.0.0",
|
||||||
"npm": ">=10.0.0"
|
"npm": ">=10.0.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "npm run gen:api && vite build",
|
"build": "npm run gen:api && vite build",
|
||||||
"preview": "vite preview",
|
"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",
|
"typecheck": "tsc --noEmit",
|
||||||
"test": "vitest run",
|
"test": "vitest run",
|
||||||
"test:watch": "vitest",
|
"test:watch": "vitest",
|
||||||
"storybook": "storybook dev -p 6006",
|
"storybook": "storybook dev -p 6006",
|
||||||
"build-storybook": "storybook build",
|
"build-storybook": "storybook build",
|
||||||
"gen": "npm run gen:zod && npm run gen:api",
|
"gen": "npm run gen:zod && npm run gen:api",
|
||||||
"gen:api": "node --experimental-strip-types --disable-warning=ExperimentalWarning scripts/build-openapi.mjs",
|
"gen:api": "node scripts/build-openapi.mjs",
|
||||||
"gen:zod": "cd .. && go run ./tools/openapigen",
|
"gen:zod": "cd .. && go run ./tools/openapigen",
|
||||||
"prepare": "cd .. && husky frontend/.husky || true"
|
"prepare": "cd .. && husky frontend/.husky || true"
|
||||||
},
|
},
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
"src/**/*.{ts,tsx}": "eslint --fix"
|
"src/**/*.{ts,tsx}": [
|
||||||
|
"oxfmt",
|
||||||
|
"oxlint --fix"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ant-design/icons": "^6.3.2",
|
"@ant-design/icons": "^6.3.2",
|
||||||
"@codemirror/lang-json": "^6.0.2",
|
"@codemirror/lang-json": "^6.0.2",
|
||||||
"@codemirror/theme-one-dark": "^6.1.3",
|
"@codemirror/theme-one-dark": "^6.1.3",
|
||||||
"@hookform/resolvers": "^5.4.0",
|
"@hookform/resolvers": "^5.9.1",
|
||||||
"@noble/hashes": "^2.2.0",
|
"@noble/hashes": "^2.3.0",
|
||||||
"@tanstack/react-query": "^5.101.2",
|
"@tanstack/react-query": "^5.101.4",
|
||||||
"@tanstack/react-query-devtools": "^5.101.2",
|
"@tanstack/react-query-devtools": "^5.101.4",
|
||||||
"antd": "^6.5.0",
|
"antd": "^6.6.1",
|
||||||
"codemirror": "^6.0.2",
|
"codemirror": "^6.0.2",
|
||||||
"dayjs": "^1.11.21",
|
"dayjs": "^1.11.23",
|
||||||
"i18next": "^26.3.6",
|
"i18next": "^26.3.6",
|
||||||
"otpauth": "^9.5.1",
|
"otpauth": "^9.5.1",
|
||||||
"persian-calendar-suite": "^1.5.5",
|
"persian-calendar-suite": "^1.5.6",
|
||||||
"react": "^19.2.7",
|
"react": "^19.2.8",
|
||||||
"react-dom": "^19.2.7",
|
"react-dom": "^19.2.8",
|
||||||
"react-hook-form": "^7.81.0",
|
"react-hook-form": "^7.85.0",
|
||||||
"react-i18next": "^17.0.9",
|
"react-i18next": "^17.0.11",
|
||||||
"react-router-dom": "^7.18.1",
|
"react-router": "^8.3.0",
|
||||||
"swagger-ui-react": "^5.32.8",
|
"swagger-ui-react": "^5.32.14",
|
||||||
"uplot": "^1.6.32",
|
"uplot": "^1.6.32",
|
||||||
"zod": "^4.4.3"
|
"zod": "^4.4.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^10.0.1",
|
"@storybook/addon-a11y": "^10.5.9",
|
||||||
"@storybook/addon-a11y": "^10.5.0",
|
"@storybook/addon-docs": "^10.5.9",
|
||||||
"@storybook/addon-docs": "^10.5.0",
|
"@storybook/addon-vitest": "^10.5.9",
|
||||||
"@storybook/react-vite": "^10.5.0",
|
"@storybook/react-vite": "^10.5.9",
|
||||||
"@testing-library/dom": "^10.4.1",
|
"@testing-library/dom": "^10.4.1",
|
||||||
"@testing-library/react": "^16.3.2",
|
"@testing-library/react": "^16.3.2",
|
||||||
"@types/react": "^19.2.17",
|
"@types/react": "^19.2.18",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.4",
|
||||||
"@types/swagger-ui-react": "^5.18.0",
|
"@types/swagger-ui-react": "^5.18.0",
|
||||||
"@vitejs/plugin-react": "^6.0.3",
|
"@vitejs/plugin-react": "^6.0.5",
|
||||||
"@vitest/coverage-v8": "^4.1.10",
|
"@vitest/browser-playwright": "4.1.11",
|
||||||
"eslint": "^10.7.0",
|
"@vitest/coverage-v8": "^4.1.11",
|
||||||
"eslint-plugin-jsx-a11y": "^6.10.2",
|
|
||||||
"eslint-plugin-react-hooks": "^7.1.1",
|
|
||||||
"globals": "^17.7.0",
|
|
||||||
"husky": "^9.1.7",
|
"husky": "^9.1.7",
|
||||||
"jsdom": "^29.1.1",
|
"jsdom": "^30.0.1",
|
||||||
"lint-staged": "^17.0.8",
|
"lint-staged": "^17.3.0",
|
||||||
"msw": "^2.15.0",
|
"msw": "^2.15.0",
|
||||||
"storybook": "^10.5.0",
|
"oxfmt": "0.64.0",
|
||||||
"typescript": "^6.0.3",
|
"oxlint": "1.79.0",
|
||||||
"typescript-eslint": "^8.63.0",
|
"oxlint-tsgolint": "^7.0.2001",
|
||||||
"vite": "8.1.4",
|
"playwright": "^1.62.1",
|
||||||
"vitest": "^4.1.10"
|
"storybook": "^10.5.9",
|
||||||
|
"typescript": "7.0.2",
|
||||||
|
"vite": "8.2.1",
|
||||||
|
"vitest": "^4.1.11"
|
||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"eslint-plugin-jsx-a11y": {
|
|
||||||
"eslint": "$eslint"
|
|
||||||
},
|
|
||||||
"dompurify": "^3.4.11",
|
"dompurify": "^3.4.11",
|
||||||
"react-copy-to-clipboard": "^5.1.1",
|
"react-copy-to-clipboard": "^5.1.1",
|
||||||
"react-inspector": "^9.0.0",
|
"react-inspector": "^9.0.0",
|
||||||
@@ -86,7 +90,11 @@
|
|||||||
"react": "^19.0.0"
|
"react": "^19.0.0"
|
||||||
},
|
},
|
||||||
"swagger-ui-react": {
|
"swagger-ui-react": {
|
||||||
"js-yaml": "^4.2.0"
|
"js-yaml": "^4.2.0",
|
||||||
|
"brace-expansion": "^5.0.9"
|
||||||
|
},
|
||||||
|
"@typeschema/valibot": {
|
||||||
|
"valibot": "^1.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"allowScripts": {
|
"allowScripts": {
|
||||||
|
|||||||
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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user