mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-08-24 20:07:13 +00:00
da01b7637d
* feat(sub): add SubBalancer model and migration Client-side JSON-subscription balancer row: remark, strategy, member inbound ids, sort order, enabled. Registered in allModels and migrationModels so AutoMigrate and SQLite->Postgres copy pick it up. * feat(sub): add SubBalancer service List/Get/Create/Update/Delete over the sub_balancers table with remark trim, strategy allowlist (leastLoad/leastPing/random) and sort-order floor. Rows are read per request by the subscription builder, so mutations need no xray restart. * feat(sub): add SubBalancer API controller and routes GET/POST /panel/api/sub-balancers, POST /:id (update), DELETE /:id and POST /:id/del alias. inboundIds bind from repeated form keys. Mounted under the /panel/api group so the existing API token + CSRF middleware cover it. * feat(sub): emit client-side balancers in JSON subscription For each enabled balancer, append one config document whose outbounds are the selected inbounds' proxy outbounds retagged under a per-balancer prefix, with routing.balancers + burstObservatory selecting it. Balancer entries interleave with inbound entries by sort order; on equal numbers the balancer follows the inbound. Skipped when disabled or no member outbound is present. * test(sub): cover SubBalancer service and JSON output Service: validation gates (remark/strategy/inbound ids/sort order) and CRUD round-trip. JSON: balancer document shape, sort interleaving with inbounds, disabled/empty skip, and member tag dedup. * feat(sub): add sub-balancers i18n keys pages.settings.subBalancers.* block (menu, title, add, desc, field labels, strategy names, sort-order help, validation messages) added to all 13 locales. * feat(sub): add SubBalancer schema and API queries Zod schema (entity + form, strategy enum, validation messages wired to i18n keys), react-query hooks for list/create/update/delete, and the sub-balancers query key. * feat(sub): add subscription balancers settings tab SubscriptionBalancersTab lists balancers (sort order, remark, strategy, inbound count, enabled toggle, edit/delete) with a form modal (remark, strategy, sort order, multi-select inbounds filtered to multi-client protocols, enabled). Wired into SettingsPage under #subscription-balancers, and the sidebar shows the entry only when JSON subscription is enabled. * test(sub): add SubBalancer form modal test Covers add-mode (no validation errors, confirm with parsed values) and edit-mode (seeds from the balancer, preserves strategy/sort order/enabled). * feat(sub): register sub-balancers in API docs and OpenAPI Adds the sub-balancers endpoint group to endpoints.ts (list/create/update/delete + POST del alias) and regenerates frontend/public/openapi.json from it. * docs: sync openapi.json with frontend docs/public/openapi.json had fallen behind frontend/public/openapi.json (fewer paths/schemas). Copy the current frontend spec so the docs site renders the full API. * docs: add subscription balancers API reference Registers the sub-balancers page (generated MDX) and adds the sub-balancers paths to docs/public/openapi.json so the page renders the list/create/update/delete operations. * feat(sub): accept roundRobin balancer strategy Add roundRobin to the model oneof tag and the service strategy allowlist, alongside leastLoad/leastPing/random. Covered by a service-level create test that fails on the old allowlist. * feat(sub): add roundRobin strategy label pages.settings.subBalancers.strategyRoundRobin added to all 13 locales. * feat(sub): expose roundRobin in balancer form Zod strategy enum, form modal label key, and table strategy colour for roundRobin. * docs(sub): list roundRobin in strategy description The create/update strategy param description now mentions roundRobin alongside the other three. * feat(sub): add subJsonObservatory setting Panel-wide JSON string carrying the burstObservatory ping config (destination, connectivity, interval, sampling, timeout, httpMethod) emitted into client-side balancer docs. Stored like subJsonMux/Rules/FinalMask. * feat(sub): wire observatory config through sub controller WithSUBJsonObservatory option; the controller calls SubJsonService.SetObservatoryConfig after construction. * feat(sub): emit observatory conditionally with configurable probes burstObservatory is emitted only for leastPing/leastLoad; random/roundRobin get none (no fallback, so an observatory would only probe for nothing). Probe params come from the subJsonObservatory setting, falling back to the built-in defaults when empty or partial. Test covers the conditional emit and the override. * feat(sub): add subJsonObservatory to AllSetting model Frontend AllSetting model and Zod schema carry the new panel-wide observatory config string. * feat(sub): add balancer observatory config card New Sub Formats tab editing destination/connectivity/interval/sampling/timeout/httpMethod, stored as JSON in subJsonObservatory. Toggle off clears the setting; the backend then falls back to defaults. * fix(sub): hide save/restart header on sub-balancers tab Sub-balancer mutations are incremental (own CRUD API, no Save, no restart), so the page-wide 'every change needs to be saved / restart the panel' banner is misleading there. The in-tab alert already explains it correctly. * feat(sub): add observatory config i18n keys pages.settings.subBalancers.observatory.* (title, desc, probe field labels and help texts) added to all 13 locales. * feat(sub): regenerate openapi for subJsonObservatory openapigen picks up the new AllSetting field; openapi.json synced into docs. * feat(sub): add observatory tab to sub-balancers Mirrors the Xray Balancers page: two tabs (Balancers + Observatory). Wires allSetting/updateSetting into the tab and adds tabBalancers / tabObservatory labels to all locales. The page Save header is shown again on this tab so the observatory config can be saved. * refactor(sub): drop observatory tab from sub-formats Now that the observatory config lives under sub-balancers, remove the duplicate tab plus its state and defaults from sub-formats. * fix(sub): add missing inboundsCount i18n key The sub-balancers table rendered the raw key path in the Inbounds column because pages.settings.subBalancers.inboundsCount was not defined. Added it to all 13 locales. * test(sub): pin disabled-inbound exclusion from balancer The balancer builds its members from the subscriber's already-filtered entry set, so an inbound disabled for that user can never surface as a member. Adds tests for both shapes (one of several disabled, and the only selected one disabled). * fix(sub): make observatory toggle honest, default connectivity off, add balancer fallback Three coupled defects on the balancer observatory surface, flagged in PR review: - The Observatory Switch wrote '' which the Go side treats as "use built-in defaults", so leastPing/leastLoad still shipped a burstObservatory the admin could no longer see or edit. The observatory is mandatory for these strategies (Xray refuses to start leastPing/leastLoad without one — verified against Xray 26.7), so the switch is relabelled to "customise probe parameters vs built-in defaults" rather than on/off: '' keeps the defaults, a stored JSON overrides them. An info Alert explains this. - Connectivity defaulted to http://www.google.com/generate_204 and an explicit {"connectivity":""} restored it, so the UI's "Leave empty to skip" was unreachable and the direct pre-check was dead on arrival on censored client networks. Default to "" and honour an explicit empty value. - routing.balancers had no fallbackTag, so a leastPing/leastLoad balancer whose probes all fail selects nothing and dispatch fails. Emit fallbackTag pointing at the first member so a probe outage degrades instead of breaking. Also skip balancer entries (kind!=0) in the member scan so a balancer can never match another balancer's row id. Tests cover each fix and fail without it. * fix(sub-balancer): localize controller toasts and reject malformed ids Route the new controller's user-facing messages through I18nWeb so non-English admins get localized toasts like every other controller, and switch parseID to strconv.Atoi rejecting ids < 1 so "12abc" and negative ids no longer coerce to a silent no-op delete that reports success. * fix(sub-balancer): enforce remark length cap server-side The model's validate:"max=256" tag was never enforced (parseSubBalancerForm binds an ad-hoc struct without validate.Struct), so a scripted API client could store an unbounded remark that is emitted verbatim as the remarks field of every affected subscriber's config. Reject len > 256 in validate() to match the frontend Zod cap. * fix(sub-balancer): exclude mtproto from balancer member picker SubJsonService.getConfig has no mtproto case, so an mtproto inbound's first outbound is "direct" and the buildBalancerConfig "tag != proxy" guard drops it — an admin could select it, save without error, and get a balancer that silently omits it (or no document at all). Drop it from the picker and fix the comment. * docs(sub-balancers): add nav entry, fix tab pointer, note mirror scope - Add "subscription-balancers" to the en reference/api meta.json pages array so the new MDX page is reachable from the sidebar (fa/ru/zh have no MDX — gen-openapi.ts emits into en only). - Fix the endpoints.ts section description from "Settings -> Subscription" to "Settings -> Sub Balancers" (the feature's own tab) and regenerate the OpenAPI spec + MDX. - Note in docs/lib/xray/subscription.ts that balancer documents are intentionally out of scope for that mirror. * style(model): trim SubBalancer comment to 2-line cap CLAUDE.md caps committed Go comment blocks at 2 lines; this one was 3. * fix(sub-balancer): parse enabled explicitly and preserve it on partial update parseSubBalancerForm treated any non-"false" value as true (so "bogus" silently enabled) and always overwrote Enabled on update, so a PATCH that omitted the toggle reset a disabled balancer back to enabled. Parse the field with strconv.ParseBool and return *bool: absent means "no change" on update and "true" on create; a malformed value is rejected as 400. Update keeps the stored Enabled when the pointer is nil. * fix(sub-balancer): clear deleted inbound from sub_balancers.InboundIds DelInbound cascaded hosts but left the deleted inbound id in every sub_balancers.InboundIds, so the balancer kept emitting a member no subscriber could resolve — a dangling outbound tag with no proxy behind it. Strip the id inside the existing delete transaction (same shape as the hosts cascade, #5648); with the last member gone the balancer stops emitting. * fix(sub-balancer): return not-found when deleting a missing balancer Delete returned the gorm result error only, which is nil when no row matched, so the controller reported success:true for an id that never existed — a stale UI row looked like a clean delete. Check RowsAffected and return a not-found error on 0 so the toast reflects reality. * style(sub): shorten leastPing/leastLoad observatory comments The observatory-emission guard comment and its test comment ran a few lines long; trim them to a couple of lines each without dropping the invariant that leastPing/leastLoad require a burst observatory. * fix(sub): validate observatory setting instead of silently dropping it SetObservatoryConfig applied whatever survived json.Unmarshal with no checks, so a bad probe URL ("not-a-url"), non-duration interval/timeout, or even unparseable JSON was either silently applied or silently ignored. Validate each field: parse durations with time.ParseDuration, require http(s) URLs for destination/connectivity, and log a warning naming the field and the bad value on every fallback — including the unmarshal error, which was a quiet return. Bad values now keep the built-in defaults instead of leaking into the emitted burstObservatory. * fix(sub): deduplicate burst-observatory defaults across Go and frontend The burst-observatory ping defaults lived in three places that had drifted: Go defaultSubBalancerObservatoryConfig (http probe, sampling 3), the Zod PingConfigSchema, and DEFAULT_BURST_OBSERVATORY (both with a connectivity pre-check URL). Align them to one set: https probe destination, sampling 2, and empty connectivity (skip the direct pre-check). The settings tab now parses the stored JSON through PingConfigSchema and seeds its default from DEFAULT_BURST_OBSERVATORY instead of carrying its own literal. * refactor(sub): extract proxy outbounds once before the balancer loop buildBalancerConfig unmarshalled every inbound document and re-extracted its first outbound on each balancer, so with B balancers and N inbound docs the same document was parsed B*N times. Pull each doc's proxy outbound in a single pre-pass over the entries and cache it per entry; buildBalancerConfig now clones the cached map before retagging, so one parse serves every balancer. Output is byte-for-byte unchanged. * fix(sub): form balancer member tags from the inbound protocol, not tcp→vless balancerTransport derived the bal-N tag suffix from the outbound's transport network and hard-coded tcp→vless, so a vmess/tcp or trojan/tcp member was mislabelled "vless" in every client config — the tag lied about the proxy type. Use the outbound's real protocol as the suffix (bal-1-vmess, bal-1-vless, bal-1-trojan, …) so the tag names the actual proxy; the selector prefix and dedup suffix are unchanged. Update the existing tag assertions and add a vmess case that fails under the old mapping. * fix(sub-balancer): default strategy to random in the create form The create-balancer form seeded strategy to 'leastLoad', but the service validate() defaults an empty strategy to 'random' and the API docs say the default is 'random' — so a freshly opened form showed leastLoad while saving without touching the field silently stored random. Align the form default to 'random' so what the admin sees is what gets persisted. * feat(api-docs): document the SubBalancer response schema The five sub-balancer endpoints carried no responseSchema, so the API docs page rendered them without a typed example. Add example: tags to every SubBalancer field, allow the struct through openapigen, and point the list (responseSchemaArray) and single-row endpoints at 'SubBalancer'. Regenerate the Zod/JSON schemas and OpenAPI doc and mirror openapi.json into docs/. * style(sub-balancer): drop whitespace-only separator lines, add final newline subBalancer.ts and SubBalancerFormModal.tsx used single-space blank lines as separators between statements and had no trailing newline. Replace them with clean empty blank lines and end each file with a newline. * fix(i18n): translate sub-balancer toasts and observatory note The sub-balancer toast messages (list/create/update/delete/invalidId) and the observatory note were left in English across 11 non-English locales (ar, es, fa, id, ja, pt-BR, tr, uk, vi, zh-CN, zh-TW) while every other key in the subBalancers block was already translated. Translate them to match the meaning and terminology of the surrounding keys in each file; the JSON structure and keys are unchanged. * fix(sub-balancer): hide disabled inbounds from the member picker The picker offered every protocol-eligible inbound regardless of its enable flag, but getInboundsBySubId filters `AND inbounds.enable = true`. A disabled member is therefore dropped from every subscriber's entries, and when it was the balancer's only member the balancer document silently stops being emitted — with nothing in the UI explaining why. TestSubJson_BalancerSkippedWhenAll MembersDisabled already documents that backend behavior. Filter the way the sibling client picker has since #5645: hide disabled inbounds, but keep one that is already selected so editing an existing balancer cannot silently drop a member. Drop the `?? []` on the useWatch result so the new useMemo dependency stays referentially stable. * style(sub): trim the balancerMemberSuffix comment to the 2-line cap Comment blocks in committed Go are capped at 2 lines; the name already carries what the function picks, so keep only the why. --------- Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com> Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
575 lines
54 KiB
Plaintext
575 lines
54 KiB
Plaintext
---
|
||
title: Clients
|
||
description: Manage clients as first-class entities that can be attached to one
|
||
or more inbounds. A single client row drives the settings.clients entry in
|
||
every inbound it belongs to. Endpoints live under /panel/api/clients.
|
||
full: true
|
||
_openapi:
|
||
preload:
|
||
- ./public/openapi.json
|
||
toc:
|
||
- depth: 2
|
||
title: List every client with its attached inbound IDs and traffic record. The
|
||
reverse field, if set, is returned as a nested JSON object (legacy
|
||
JSON-encoded-string form is still accepted on write).
|
||
url: '#list-every-client-with-its-attached-inbound-ids-and-traffic-record-the-reverse-field-if-set-is-returned-as-a-nested-json-object-legacy-json-encoded-string-form-is-still-accepted-on-write'
|
||
- depth: 2
|
||
title: 'Filter, sort, and paginate clients on the server. Each item is a slim
|
||
row (no uuid/password/auth/flow/security/reverse/tgId) so the clients
|
||
page can ship 25-ish rows in a few KB instead of the full table. The
|
||
response also includes a summary computed across the full DB row set so
|
||
dashboard counters stay stable as the user paginates or filters: the
|
||
*Count fields are exact, while the email arrays beside them stop at 200
|
||
entries so the payload does not grow with the panel. Page size capped at
|
||
200; fetch /get/:email to obtain the full per-client payload for an
|
||
edit/info modal.'
|
||
url: '#filter-sort-and-paginate-clients-on-the-server-each-item-is-a-slim-row-no-uuidpasswordauthflowsecurityreversetgid-so-the-clients-page-can-ship-25-ish-rows-in-a-few-kb-instead-of-the-full-table-the-response-also-includes-a-summary-computed-across-the-full-db-row-set-so-dashboard-counters-stay-stable-as-the-user-paginates-or-filters-the-count-fields-are-exact-while-the-email-arrays-beside-them-stop-at-200-entries-so-the-payload-does-not-grow-with-the-panel-page-size-capped-at-200-fetch-getemail-to-obtain-the-full-per-client-payload-for-an-editinfo-modal'
|
||
- depth: 2
|
||
title: Fetch one client by email, including the inbound IDs and external config
|
||
IDs it is attached to.
|
||
url: '#fetch-one-client-by-email-including-the-inbound-ids-and-external-config-ids-it-is-attached-to'
|
||
- depth: 2
|
||
title: Fetch clients by Telegram user ID. Returns an array since multiple
|
||
clients can share the same Telegram ID.
|
||
url: '#fetch-clients-by-telegram-user-id-returns-an-array-since-multiple-clients-can-share-the-same-telegram-id'
|
||
- depth: 2
|
||
title: Create a new client and attach it to one or more inbounds in a single
|
||
call. Body is JSON. Per-protocol secrets are generated server-side when
|
||
omitted, so callers can send only the universal fields.
|
||
url: '#create-a-new-client-and-attach-it-to-one-or-more-inbounds-in-a-single-call-body-is-json-per-protocol-secrets-are-generated-server-side-when-omitted-so-callers-can-send-only-the-universal-fields'
|
||
- depth: 2
|
||
title: Update an existing client by email. Changes propagate to every attached
|
||
inbound. Body is the JSON client payload — supply the full set of fields
|
||
you want to keep (the server replaces the row, it does not patch).
|
||
url: '#update-an-existing-client-by-email-changes-propagate-to-every-attached-inbound-body-is-the-json-client-payload--supply-the-full-set-of-fields-you-want-to-keep-the-server-replaces-the-row-it-does-not-patch'
|
||
- depth: 2
|
||
title: Delete a client by email. Removes it from every attached inbound and
|
||
drops its traffic record unless keepTraffic=1 is passed.
|
||
url: '#delete-a-client-by-email-removes-it-from-every-attached-inbound-and-drops-its-traffic-record-unless-keeptraffic1-is-passed'
|
||
- depth: 2
|
||
title: Attach an existing client to one or more additional inbounds. Body is
|
||
JSON.
|
||
url: '#attach-an-existing-client-to-one-or-more-additional-inbounds-body-is-json'
|
||
- depth: 2
|
||
title: Detach a client from one or more inbounds without deleting the client.
|
||
url: '#detach-a-client-from-one-or-more-inbounds-without-deleting-the-client'
|
||
- depth: 2
|
||
title: Replace a client's external links and external subscriptions. Sends the
|
||
full set; the server replaces all rows. Disabled rows stay saved for
|
||
editing but are not emitted in generated subscriptions.
|
||
url: '#replace-a-clients-external-links-and-external-subscriptions-sends-the-full-set-the-server-replaces-all-rows-disabled-rows-stay-saved-for-editing-but-are-not-emitted-in-generated-subscriptions'
|
||
- depth: 2
|
||
title: Reset the up/down counters for every client globally. Quotas and expiry
|
||
are not affected. Triggers an Xray restart if any counter actually
|
||
moved.
|
||
url: '#reset-the-updown-counters-for-every-client-globally-quotas-and-expiry-are-not-affected-triggers-an-xray-restart-if-any-counter-actually-moved'
|
||
- depth: 2
|
||
title: Delete every client whose traffic quota is exhausted (used >= total, when
|
||
reset is disabled) or whose expiry has passed. Returns the deleted count
|
||
and triggers an Xray restart when any client was on a running inbound.
|
||
url: '#delete-every-client-whose-traffic-quota-is-exhausted-used--total-when-reset-is-disabled-or-whose-expiry-has-passed-returns-the-deleted-count-and-triggers-an-xray-restart-when-any-client-was-on-a-running-inbound'
|
||
- depth: 2
|
||
title: Delete every client that is not attached to any inbound, along with its
|
||
traffic record, IP log, HWID devices, and external links. Useful for
|
||
clearing clients left unattached after their inbounds were removed.
|
||
Returns the deleted count. Cannot be undone.
|
||
url: '#delete-every-client-that-is-not-attached-to-any-inbound-along-with-its-traffic-record-ip-log-hwid-devices-and-external-links-useful-for-clearing-clients-left-unattached-after-their-inbounds-were-removed-returns-the-deleted-count-cannot-be-undone'
|
||
- depth: 2
|
||
title: Return every client as a {client, inboundIds} array — the same shape
|
||
/bulkCreate and /import accept — so the payload round-trips straight
|
||
back through /import. Clients with no inbound attachment are included
|
||
with an empty inboundIds list. The UI shows this in a CodeMirror viewer
|
||
(copy / download); programmatic callers get the array in obj.
|
||
url: '#return-every-client-as-a-client-inboundids-array--the-same-shape-bulkcreate-and-import-accept--so-the-payload-round-trips-straight-back-through-import-clients-with-no-inbound-attachment-are-included-with-an-empty-inboundids-list-the-ui-shows-this-in-a-codemirror-viewer-copy--download-programmatic-callers-get-the-array-in-obj'
|
||
- depth: 2
|
||
title: 'Import clients from a JSON body { "data": "<json>" }, where data is a
|
||
string-encoded array produced by /export ([{client, inboundIds}]). Items
|
||
with inboundIds are created and attached to those inbounds; items with
|
||
an empty inboundIds list are restored as unattached client records.
|
||
Existing emails are never overwritten — they are returned in skipped.
|
||
Triggers a single Xray restart at the end if any target inbound was
|
||
running.'
|
||
url: '#import-clients-from-a-json-body--data-json--where-data-is-a-string-encoded-array-produced-by-export-client-inboundids-items-with-inboundids-are-created-and-attached-to-those-inbounds-items-with-an-empty-inboundids-list-are-restored-as-unattached-client-records-existing-emails-are-never-overwritten--they-are-returned-in-skipped-triggers-a-single-xray-restart-at-the-end-if-any-target-inbound-was-running'
|
||
- depth: 2
|
||
title: 'Shift expiry and/or traffic quota for many clients in one call.
|
||
addDays/addBytes may be negative. Clients with unlimited expiry
|
||
(expiryTime=0) or unlimited traffic (totalGB=0) are skipped for the
|
||
corresponding field — bulk extend never converts unlimited to limited. A
|
||
client that was auto-disabled solely because it was depleted (expired or
|
||
over quota) is automatically re-enabled — locally and on its node — when
|
||
the adjustment lifts it out of depletion; a manually-disabled or
|
||
still-depleted client is left disabled. The optional flow directive sets
|
||
the XTLS flow on every client: "none" clears it,
|
||
"xtls-rprx-vision"/"xtls-rprx-vision-udp443" set it where the inbound
|
||
supports it (omit or "" to leave it unchanged). Returns the adjusted
|
||
count and per-email skip reasons.'
|
||
url: '#shift-expiry-andor-traffic-quota-for-many-clients-in-one-call-adddaysaddbytes-may-be-negative-clients-with-unlimited-expiry-expirytime0-or-unlimited-traffic-totalgb0-are-skipped-for-the-corresponding-field--bulk-extend-never-converts-unlimited-to-limited-a-client-that-was-auto-disabled-solely-because-it-was-depleted-expired-or-over-quota-is-automatically-re-enabled--locally-and-on-its-node--when-the-adjustment-lifts-it-out-of-depletion-a-manually-disabled-or-still-depleted-client-is-left-disabled-the-optional-flow-directive-sets-the-xtls-flow-on-every-client-none-clears-it-xtls-rprx-visionxtls-rprx-vision-udp443-set-it-where-the-inbound-supports-it-omit-or--to-leave-it-unchanged-returns-the-adjusted-count-and-per-email-skip-reasons'
|
||
- depth: 2
|
||
title: Enable many clients in one call. Emails are grouped by inbound and
|
||
applied with a single read-modify-write per inbound; the running Xray
|
||
(local or remote node) is updated to add each user. Note that enabling a
|
||
client whose quota is exhausted or whose expiry has passed only flips
|
||
the flag — the traffic loop will disable it again on the next tick.
|
||
Returns the changed count and per-email skip reasons.
|
||
url: '#enable-many-clients-in-one-call-emails-are-grouped-by-inbound-and-applied-with-a-single-read-modify-write-per-inbound-the-running-xray-local-or-remote-node-is-updated-to-add-each-user-note-that-enabling-a-client-whose-quota-is-exhausted-or-whose-expiry-has-passed-only-flips-the-flag--the-traffic-loop-will-disable-it-again-on-the-next-tick-returns-the-changed-count-and-per-email-skip-reasons'
|
||
- depth: 2
|
||
title: Disable many clients in one call. Emails are grouped by inbound and
|
||
applied with a single read-modify-write per inbound; the running Xray
|
||
(local or remote node) is updated to remove each user. Returns the
|
||
changed count and per-email skip reasons.
|
||
url: '#disable-many-clients-in-one-call-emails-are-grouped-by-inbound-and-applied-with-a-single-read-modify-write-per-inbound-the-running-xray-local-or-remote-node-is-updated-to-remove-each-user-returns-the-changed-count-and-per-email-skip-reasons'
|
||
- depth: 2
|
||
title: Delete many clients in one call. The server processes the list
|
||
sequentially so each delete sees the committed state of the previous one
|
||
— avoids the race the per-email fan-out had on the panel side. Pass
|
||
keepTraffic=true to retain the xray_client_traffic rows after deletion.
|
||
url: '#delete-many-clients-in-one-call-the-server-processes-the-list-sequentially-so-each-delete-sees-the-committed-state-of-the-previous-one--avoids-the-race-the-per-email-fan-out-had-on-the-panel-side-pass-keeptraffictrue-to-retain-the-xray_client_traffic-rows-after-deletion'
|
||
- depth: 2
|
||
title: Create many clients in one call. Body is a JSON array of {client,
|
||
inboundIds} payloads — the same shape /add accepts. Items are processed
|
||
sequentially; per-email skip reasons are returned for items that fail
|
||
(e.g., duplicate email). Triggers a single Xray restart at the end if
|
||
any inbound was running.
|
||
url: '#create-many-clients-in-one-call-body-is-a-json-array-of-client-inboundids-payloads--the-same-shape-add-accepts-items-are-processed-sequentially-per-email-skip-reasons-are-returned-for-items-that-fail-eg-duplicate-email-triggers-a-single-xray-restart-at-the-end-if-any-inbound-was-running'
|
||
- depth: 2
|
||
title: Add many clients to a group in one call. Updates clients.group_name and
|
||
patches the matching client entry inside every owning inbound's settings
|
||
JSON in a single transaction. If the group name does not yet exist (in
|
||
client_groups or as a derived label), it is auto-created as a persistent
|
||
group. To clear the group label, use /groups/bulkRemove instead.
|
||
url: '#add-many-clients-to-a-group-in-one-call-updates-clientsgroup_name-and-patches-the-matching-client-entry-inside-every-owning-inbounds-settings-json-in-a-single-transaction-if-the-group-name-does-not-yet-exist-in-client_groups-or-as-a-derived-label-it-is-auto-created-as-a-persistent-group-to-clear-the-group-label-use-groupsbulkremove-instead'
|
||
- depth: 2
|
||
title: Clear the group label on many clients in one call. Inverse of
|
||
/groups/bulkAdd. Clients themselves are kept — only the group label is
|
||
cleared from clients.group_name and from each owning inbound's settings
|
||
JSON. Groups become empty if all their members are removed.
|
||
url: '#clear-the-group-label-on-many-clients-in-one-call-inverse-of-groupsbulkadd-clients-themselves-are-kept--only-the-group-label-is-cleared-from-clientsgroup_name-and-from-each-owning-inbounds-settings-json-groups-become-empty-if-all-their-members-are-removed'
|
||
- depth: 2
|
||
title: Attach many existing clients to many inbounds in one call. Each client
|
||
keeps its identity (email/UUID/password/subId) and a shared traffic row;
|
||
all clients are added to a target inbound in a single AddInboundClient
|
||
call. Clients already present on a target are reported under skipped.
|
||
Returns per-email attached/skipped/errors lists and triggers a single
|
||
Xray restart if any target inbound was running.
|
||
url: '#attach-many-existing-clients-to-many-inbounds-in-one-call-each-client-keeps-its-identity-emailuuidpasswordsubid-and-a-shared-traffic-row-all-clients-are-added-to-a-target-inbound-in-a-single-addinboundclient-call-clients-already-present-on-a-target-are-reported-under-skipped-returns-per-email-attachedskippederrors-lists-and-triggers-a-single-xray-restart-if-any-target-inbound-was-running'
|
||
- depth: 2
|
||
title: "Mirror of bulkAttach: detach many existing clients from many inbounds in
|
||
one call. For each email, intersects the client's current inbounds with
|
||
the requested set and detaches from those only; (email, inbound) pairs
|
||
where the client is not currently attached are silently no-ops. Emails
|
||
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 detached/skipped/errors
|
||
lists and triggers a single Xray restart if any target inbound was
|
||
running."
|
||
url: '#mirror-of-bulkattach-detach-many-existing-clients-from-many-inbounds-in-one-call-for-each-email-intersects-the-clients-current-inbounds-with-the-requested-set-and-detaches-from-those-only-email-inbound-pairs-where-the-client-is-not-currently-attached-are-silently-no-ops-emails-not-attached-to-any-of-the-requested-inbounds-are-reported-under-skipped-client-records-are-kept-even-if-they-become-orphaned--use-bulkdel-for-full-removal-returns-per-email-detachedskippederrors-lists-and-triggers-a-single-xray-restart-if-any-target-inbound-was-running'
|
||
- depth: 2
|
||
title: Zero up/down counters for many clients in one call. Loops the
|
||
single-reset path so each client is re-enabled across its attached
|
||
inbounds and pushed to Xray/remote nodes. Returns the count of
|
||
successfully reset clients.
|
||
url: '#zero-updown-counters-for-many-clients-in-one-call-loops-the-single-reset-path-so-each-client-is-re-enabled-across-its-attached-inbounds-and-pushed-to-xrayremote-nodes-returns-the-count-of-successfully-reset-clients'
|
||
- depth: 2
|
||
title: List all client groups with their member counts. Merges persisted groups
|
||
(rows in client_groups, including empty placeholders) with the distinct
|
||
group_name values currently set on clients. Sorted alphabetically
|
||
(case-insensitive).
|
||
url: '#list-all-client-groups-with-their-member-counts-merges-persisted-groups-rows-in-client_groups-including-empty-placeholders-with-the-distinct-group_name-values-currently-set-on-clients-sorted-alphabetically-case-insensitive'
|
||
- depth: 2
|
||
title: Return just the email list of clients that currently belong to the given
|
||
group. Useful for fanning a single bulk action over an entire group
|
||
without round-tripping the full client list.
|
||
url: '#return-just-the-email-list-of-clients-that-currently-belong-to-the-given-group-useful-for-fanning-a-single-bulk-action-over-an-entire-group-without-round-tripping-the-full-client-list'
|
||
- depth: 2
|
||
title: Create a new empty (placeholder) group. The group becomes selectable in
|
||
client forms and the filter drawer even before any client is added to
|
||
it. Errors if a group with the same name already exists.
|
||
url: '#create-a-new-empty-placeholder-group-the-group-becomes-selectable-in-client-forms-and-the-filter-drawer-even-before-any-client-is-added-to-it-errors-if-a-group-with-the-same-name-already-exists'
|
||
- depth: 2
|
||
title: Rename a group. The new name is applied to the client_groups row AND
|
||
propagated to every matching client (both clients.group_name and the
|
||
client entry inside every owning inbound's settings JSON) in a single
|
||
transaction. Returns the number of clients whose label was updated.
|
||
url: '#rename-a-group-the-new-name-is-applied-to-the-client_groups-row-and-propagated-to-every-matching-client-both-clientsgroup_name-and-the-client-entry-inside-every-owning-inbounds-settings-json-in-a-single-transaction-returns-the-number-of-clients-whose-label-was-updated'
|
||
- depth: 2
|
||
title: Remove a group. Deletes the client_groups row and clears the group label
|
||
from every matching client (both clients.group_name and the inbound
|
||
settings JSON). The clients themselves are NOT deleted — use /bulkDel
|
||
after filtering by group for that. Returns the count of clients whose
|
||
label was cleared.
|
||
url: '#remove-a-group-deletes-the-client_groups-row-and-clears-the-group-label-from-every-matching-client-both-clientsgroup_name-and-the-inbound-settings-json-the-clients-themselves-are-not-deleted--use-bulkdel-after-filtering-by-group-for-that-returns-the-count-of-clients-whose-label-was-cleared'
|
||
- depth: 2
|
||
title: Reset only the group-level traffic counter shown on the groups page.
|
||
Snapshots the current up/down sum of the group's members as a baseline
|
||
so the group total reads zero, while leaving each client's own counters
|
||
(and their quotas) untouched. No Xray restart is triggered. Creates the
|
||
client_groups row if the group exists only as a derived label.
|
||
url: '#reset-only-the-group-level-traffic-counter-shown-on-the-groups-page-snapshots-the-current-updown-sum-of-the-groups-members-as-a-baseline-so-the-group-total-reads-zero-while-leaving-each-clients-own-counters-and-their-quotas-untouched-no-xray-restart-is-triggered-creates-the-client_groups-row-if-the-group-exists-only-as-a-derived-label'
|
||
- depth: 2
|
||
title: Zero out a single client’s up/down counters. Re-enables the client across
|
||
every attached inbound and pushes the change to Xray (or the remote
|
||
node) so depleted users can connect again immediately.
|
||
url: '#zero-out-a-single-clients-updown-counters-re-enables-the-client-across-every-attached-inbound-and-pushes-the-change-to-xray-or-the-remote-node-so-depleted-users-can-connect-again-immediately'
|
||
- depth: 2
|
||
title: Manually adjust a client’s upload + download counters. Useful for
|
||
migrations from external accounting systems.
|
||
url: '#manually-adjust-a-clients-upload--download-counters-useful-for-migrations-from-external-accounting-systems'
|
||
- depth: 2
|
||
title: List source IPs that have connected with the given client’s credentials.
|
||
Returns an array of "ip (timestamp)" strings.
|
||
url: '#list-source-ips-that-have-connected-with-the-given-clients-credentials-returns-an-array-of-ip-timestamp-strings'
|
||
- depth: 2
|
||
title: Reset the recorded IP list for a client.
|
||
url: '#reset-the-recorded-ip-list-for-a-client'
|
||
- depth: 2
|
||
title: List registered HWID devices for a client. Hashes are not exposed.
|
||
url: '#list-registered-hwid-devices-for-a-client-hashes-are-not-exposed'
|
||
- depth: 2
|
||
title: Clear all registered HWID devices for a client so new devices can
|
||
register again.
|
||
url: '#clear-all-registered-hwid-devices-for-a-client-so-new-devices-can-register-again'
|
||
- depth: 2
|
||
title: Remove a single registered HWID device by its id, freeing one slot under
|
||
the HWID limit.
|
||
url: '#remove-a-single-registered-hwid-device-by-its-id-freeing-one-slot-under-the-hwid-limit'
|
||
- depth: 2
|
||
title: List the emails of currently connected clients (last seen within the
|
||
heartbeat window), deduped across every node.
|
||
url: '#list-the-emails-of-currently-connected-clients-last-seen-within-the-heartbeat-window-deduped-across-every-node'
|
||
- depth: 2
|
||
title: Online client emails grouped by the panelGuid of the node that physically
|
||
hosts each client. The local panel uses its own GUID; each node (at any
|
||
depth in a chain) uses its GUID. Lets the inbounds page attribute online
|
||
status to the real node instead of the intermediate one it syncs
|
||
through.
|
||
url: '#online-client-emails-grouped-by-the-panelguid-of-the-node-that-physically-hosts-each-client-the-local-panel-uses-its-own-guid-each-node-at-any-depth-in-a-chain-uses-its-guid-lets-the-inbounds-page-attribute-online-status-to-the-real-node-instead-of-the-intermediate-one-it-syncs-through'
|
||
- depth: 2
|
||
title: Per-client source IPs grouped by the panelGuid of the node that observed
|
||
them. Lets the central panel attribute and enforce per-client IP limits
|
||
using the real visitor IPs each node sees, instead of the address of the
|
||
intermediate panel it syncs through.
|
||
url: '#per-client-source-ips-grouped-by-the-panelguid-of-the-node-that-observed-them-lets-the-central-panel-attribute-and-enforce-per-client-ip-limits-using-the-real-visitor-ips-each-node-sees-instead-of-the-address-of-the-intermediate-panel-it-syncs-through'
|
||
- depth: 2
|
||
title: Inbound tags that carried traffic within the heartbeat window, grouped by
|
||
the hosting node's panelGuid. Pairs with onlinesByGuid so the inbounds
|
||
page only marks a multi-inbound client online on the inbounds it
|
||
actually used. Nodes that do not report per-inbound activity are absent.
|
||
url: '#inbound-tags-that-carried-traffic-within-the-heartbeat-window-grouped-by-the-hosting-nodes-panelguid-pairs-with-onlinesbyguid-so-the-inbounds-page-only-marks-a-multi-inbound-client-online-on-the-inbounds-it-actually-used-nodes-that-do-not-report-per-inbound-activity-are-absent'
|
||
- depth: 2
|
||
title: Map of client email → last-seen unix timestamp.
|
||
url: '#map-of-client-email--last-seen-unix-timestamp'
|
||
- depth: 2
|
||
title: Traffic counters for a client identified by email.
|
||
url: '#traffic-counters-for-a-client-identified-by-email'
|
||
- depth: 2
|
||
title: Return every protocol URL (vless://, vmess://, trojan://, ss://,
|
||
hysteria://, hy2://) for clients matching the subscription ID. Same
|
||
result set as /sub/<subId>, but as a JSON array — no base64. When an
|
||
inbound has streamSettings.externalProxy set, one URL is emitted per
|
||
external proxy. Empty array when the subId has no enabled clients.
|
||
url: '#return-every-protocol-url-vless-vmess-trojan-ss-hysteria-hy2-for-clients-matching-the-subscription-id-same-result-set-as-subsubid-but-as-a-json-array--no-base64-when-an-inbound-has-streamsettingsexternalproxy-set-one-url-is-emitted-per-external-proxy-empty-array-when-the-subid-has-no-enabled-clients'
|
||
- depth: 2
|
||
title: 'Return every URL for one client across all attached inbounds — the same
|
||
strings the Copy URL button copies in the panel UI. Supported protocols:
|
||
vmess, vless, trojan, shadowsocks, hysteria. If
|
||
streamSettings.externalProxy is set, returns one URL per external proxy.
|
||
Protocols without a URL form (socks, http, mixed, wireguard, dokodemo,
|
||
tunnel) contribute nothing.'
|
||
url: '#return-every-url-for-one-client-across-all-attached-inbounds--the-same-strings-the-copy-url-button-copies-in-the-panel-ui-supported-protocols-vmess-vless-trojan-shadowsocks-hysteria-if-streamsettingsexternalproxy-is-set-returns-one-url-per-external-proxy-protocols-without-a-url-form-socks-http-mixed-wireguard-dokodemo-tunnel-contribute-nothing'
|
||
structuredData:
|
||
headings:
|
||
- content: List every client with its attached inbound IDs and traffic record. The
|
||
reverse field, if set, is returned as a nested JSON object (legacy
|
||
JSON-encoded-string form is still accepted on write).
|
||
id: list-every-client-with-its-attached-inbound-ids-and-traffic-record-the-reverse-field-if-set-is-returned-as-a-nested-json-object-legacy-json-encoded-string-form-is-still-accepted-on-write
|
||
- content: 'Filter, sort, and paginate clients on the server. Each item is a slim
|
||
row (no uuid/password/auth/flow/security/reverse/tgId) so the clients
|
||
page can ship 25-ish rows in a few KB instead of the full table. The
|
||
response also includes a summary computed across the full DB row set
|
||
so dashboard counters stay stable as the user paginates or filters:
|
||
the *Count fields are exact, while the email arrays beside them stop
|
||
at 200 entries so the payload does not grow with the panel. Page size
|
||
capped at 200; fetch /get/:email to obtain the full per-client payload
|
||
for an edit/info modal.'
|
||
id: filter-sort-and-paginate-clients-on-the-server-each-item-is-a-slim-row-no-uuidpasswordauthflowsecurityreversetgid-so-the-clients-page-can-ship-25-ish-rows-in-a-few-kb-instead-of-the-full-table-the-response-also-includes-a-summary-computed-across-the-full-db-row-set-so-dashboard-counters-stay-stable-as-the-user-paginates-or-filters-the-count-fields-are-exact-while-the-email-arrays-beside-them-stop-at-200-entries-so-the-payload-does-not-grow-with-the-panel-page-size-capped-at-200-fetch-getemail-to-obtain-the-full-per-client-payload-for-an-editinfo-modal
|
||
- content: Fetch one client by email, including the inbound IDs and external
|
||
config IDs it is attached to.
|
||
id: fetch-one-client-by-email-including-the-inbound-ids-and-external-config-ids-it-is-attached-to
|
||
- content: Fetch clients by Telegram user ID. Returns an array since multiple
|
||
clients can share the same Telegram ID.
|
||
id: fetch-clients-by-telegram-user-id-returns-an-array-since-multiple-clients-can-share-the-same-telegram-id
|
||
- content: Create a new client and attach it to one or more inbounds in a single
|
||
call. Body is JSON. Per-protocol secrets are generated server-side
|
||
when omitted, so callers can send only the universal fields.
|
||
id: create-a-new-client-and-attach-it-to-one-or-more-inbounds-in-a-single-call-body-is-json-per-protocol-secrets-are-generated-server-side-when-omitted-so-callers-can-send-only-the-universal-fields
|
||
- content: Update an existing client by email. Changes propagate to every attached
|
||
inbound. Body is the JSON client payload — supply the full set of
|
||
fields you want to keep (the server replaces the row, it does not
|
||
patch).
|
||
id: update-an-existing-client-by-email-changes-propagate-to-every-attached-inbound-body-is-the-json-client-payload--supply-the-full-set-of-fields-you-want-to-keep-the-server-replaces-the-row-it-does-not-patch
|
||
- content: Delete a client by email. Removes it from every attached inbound and
|
||
drops its traffic record unless keepTraffic=1 is passed.
|
||
id: delete-a-client-by-email-removes-it-from-every-attached-inbound-and-drops-its-traffic-record-unless-keeptraffic1-is-passed
|
||
- content: Attach an existing client to one or more additional inbounds. Body is
|
||
JSON.
|
||
id: attach-an-existing-client-to-one-or-more-additional-inbounds-body-is-json
|
||
- content: Detach a client from one or more inbounds without deleting the client.
|
||
id: detach-a-client-from-one-or-more-inbounds-without-deleting-the-client
|
||
- content: Replace a client's external links and external subscriptions. Sends the
|
||
full set; the server replaces all rows. Disabled rows stay saved for
|
||
editing but are not emitted in generated subscriptions.
|
||
id: replace-a-clients-external-links-and-external-subscriptions-sends-the-full-set-the-server-replaces-all-rows-disabled-rows-stay-saved-for-editing-but-are-not-emitted-in-generated-subscriptions
|
||
- content: Reset the up/down counters for every client globally. Quotas and expiry
|
||
are not affected. Triggers an Xray restart if any counter actually
|
||
moved.
|
||
id: reset-the-updown-counters-for-every-client-globally-quotas-and-expiry-are-not-affected-triggers-an-xray-restart-if-any-counter-actually-moved
|
||
- content: Delete every client whose traffic quota is exhausted (used >= total,
|
||
when reset is disabled) or whose expiry has passed. Returns the
|
||
deleted count and triggers an Xray restart when any client was on a
|
||
running inbound.
|
||
id: delete-every-client-whose-traffic-quota-is-exhausted-used--total-when-reset-is-disabled-or-whose-expiry-has-passed-returns-the-deleted-count-and-triggers-an-xray-restart-when-any-client-was-on-a-running-inbound
|
||
- content: Delete every client that is not attached to any inbound, along with its
|
||
traffic record, IP log, HWID devices, and external links. Useful for
|
||
clearing clients left unattached after their inbounds were removed.
|
||
Returns the deleted count. Cannot be undone.
|
||
id: delete-every-client-that-is-not-attached-to-any-inbound-along-with-its-traffic-record-ip-log-hwid-devices-and-external-links-useful-for-clearing-clients-left-unattached-after-their-inbounds-were-removed-returns-the-deleted-count-cannot-be-undone
|
||
- content: Return every client as a {client, inboundIds} array — the same shape
|
||
/bulkCreate and /import accept — so the payload round-trips straight
|
||
back through /import. Clients with no inbound attachment are included
|
||
with an empty inboundIds list. The UI shows this in a CodeMirror
|
||
viewer (copy / download); programmatic callers get the array in obj.
|
||
id: return-every-client-as-a-client-inboundids-array--the-same-shape-bulkcreate-and-import-accept--so-the-payload-round-trips-straight-back-through-import-clients-with-no-inbound-attachment-are-included-with-an-empty-inboundids-list-the-ui-shows-this-in-a-codemirror-viewer-copy--download-programmatic-callers-get-the-array-in-obj
|
||
- content: 'Import clients from a JSON body { "data": "<json>" }, where data is a
|
||
string-encoded array produced by /export ([{client, inboundIds}]).
|
||
Items with inboundIds are created and attached to those inbounds;
|
||
items with an empty inboundIds list are restored as unattached client
|
||
records. Existing emails are never overwritten — they are returned in
|
||
skipped. Triggers a single Xray restart at the end if any target
|
||
inbound was running.'
|
||
id: import-clients-from-a-json-body--data-json--where-data-is-a-string-encoded-array-produced-by-export-client-inboundids-items-with-inboundids-are-created-and-attached-to-those-inbounds-items-with-an-empty-inboundids-list-are-restored-as-unattached-client-records-existing-emails-are-never-overwritten--they-are-returned-in-skipped-triggers-a-single-xray-restart-at-the-end-if-any-target-inbound-was-running
|
||
- content: 'Shift expiry and/or traffic quota for many clients in one call.
|
||
addDays/addBytes may be negative. Clients with unlimited expiry
|
||
(expiryTime=0) or unlimited traffic (totalGB=0) are skipped for the
|
||
corresponding field — bulk extend never converts unlimited to limited.
|
||
A client that was auto-disabled solely because it was depleted
|
||
(expired or over quota) is automatically re-enabled — locally and on
|
||
its node — when the adjustment lifts it out of depletion; a
|
||
manually-disabled or still-depleted client is left disabled. The
|
||
optional flow directive sets the XTLS flow on every client: "none"
|
||
clears it, "xtls-rprx-vision"/"xtls-rprx-vision-udp443" set it where
|
||
the inbound supports it (omit or "" to leave it unchanged). Returns
|
||
the adjusted count and per-email skip reasons.'
|
||
id: shift-expiry-andor-traffic-quota-for-many-clients-in-one-call-adddaysaddbytes-may-be-negative-clients-with-unlimited-expiry-expirytime0-or-unlimited-traffic-totalgb0-are-skipped-for-the-corresponding-field--bulk-extend-never-converts-unlimited-to-limited-a-client-that-was-auto-disabled-solely-because-it-was-depleted-expired-or-over-quota-is-automatically-re-enabled--locally-and-on-its-node--when-the-adjustment-lifts-it-out-of-depletion-a-manually-disabled-or-still-depleted-client-is-left-disabled-the-optional-flow-directive-sets-the-xtls-flow-on-every-client-none-clears-it-xtls-rprx-visionxtls-rprx-vision-udp443-set-it-where-the-inbound-supports-it-omit-or--to-leave-it-unchanged-returns-the-adjusted-count-and-per-email-skip-reasons
|
||
- content: Enable many clients in one call. Emails are grouped by inbound and
|
||
applied with a single read-modify-write per inbound; the running Xray
|
||
(local or remote node) is updated to add each user. Note that enabling
|
||
a client whose quota is exhausted or whose expiry has passed only
|
||
flips the flag — the traffic loop will disable it again on the next
|
||
tick. Returns the changed count and per-email skip reasons.
|
||
id: enable-many-clients-in-one-call-emails-are-grouped-by-inbound-and-applied-with-a-single-read-modify-write-per-inbound-the-running-xray-local-or-remote-node-is-updated-to-add-each-user-note-that-enabling-a-client-whose-quota-is-exhausted-or-whose-expiry-has-passed-only-flips-the-flag--the-traffic-loop-will-disable-it-again-on-the-next-tick-returns-the-changed-count-and-per-email-skip-reasons
|
||
- content: Disable many clients in one call. Emails are grouped by inbound and
|
||
applied with a single read-modify-write per inbound; the running Xray
|
||
(local or remote node) is updated to remove each user. Returns the
|
||
changed count and per-email skip reasons.
|
||
id: disable-many-clients-in-one-call-emails-are-grouped-by-inbound-and-applied-with-a-single-read-modify-write-per-inbound-the-running-xray-local-or-remote-node-is-updated-to-remove-each-user-returns-the-changed-count-and-per-email-skip-reasons
|
||
- content: Delete many clients in one call. The server processes the list
|
||
sequentially so each delete sees the committed state of the previous
|
||
one — avoids the race the per-email fan-out had on the panel side.
|
||
Pass keepTraffic=true to retain the xray_client_traffic rows after
|
||
deletion.
|
||
id: delete-many-clients-in-one-call-the-server-processes-the-list-sequentially-so-each-delete-sees-the-committed-state-of-the-previous-one--avoids-the-race-the-per-email-fan-out-had-on-the-panel-side-pass-keeptraffictrue-to-retain-the-xray_client_traffic-rows-after-deletion
|
||
- content: Create many clients in one call. Body is a JSON array of {client,
|
||
inboundIds} payloads — the same shape /add accepts. Items are
|
||
processed sequentially; per-email skip reasons are returned for items
|
||
that fail (e.g., duplicate email). Triggers a single Xray restart at
|
||
the end if any inbound was running.
|
||
id: create-many-clients-in-one-call-body-is-a-json-array-of-client-inboundids-payloads--the-same-shape-add-accepts-items-are-processed-sequentially-per-email-skip-reasons-are-returned-for-items-that-fail-eg-duplicate-email-triggers-a-single-xray-restart-at-the-end-if-any-inbound-was-running
|
||
- content: Add many clients to a group in one call. Updates clients.group_name and
|
||
patches the matching client entry inside every owning inbound's
|
||
settings JSON in a single transaction. If the group name does not yet
|
||
exist (in client_groups or as a derived label), it is auto-created as
|
||
a persistent group. To clear the group label, use /groups/bulkRemove
|
||
instead.
|
||
id: add-many-clients-to-a-group-in-one-call-updates-clientsgroup_name-and-patches-the-matching-client-entry-inside-every-owning-inbounds-settings-json-in-a-single-transaction-if-the-group-name-does-not-yet-exist-in-client_groups-or-as-a-derived-label-it-is-auto-created-as-a-persistent-group-to-clear-the-group-label-use-groupsbulkremove-instead
|
||
- content: Clear the group label on many clients in one call. Inverse of
|
||
/groups/bulkAdd. Clients themselves are kept — only the group label is
|
||
cleared from clients.group_name and from each owning inbound's
|
||
settings JSON. Groups become empty if all their members are removed.
|
||
id: clear-the-group-label-on-many-clients-in-one-call-inverse-of-groupsbulkadd-clients-themselves-are-kept--only-the-group-label-is-cleared-from-clientsgroup_name-and-from-each-owning-inbounds-settings-json-groups-become-empty-if-all-their-members-are-removed
|
||
- content: Attach many existing clients to many inbounds in one call. Each client
|
||
keeps its identity (email/UUID/password/subId) and a shared traffic
|
||
row; all clients are added to a target inbound in a single
|
||
AddInboundClient call. Clients already present on a target are
|
||
reported under skipped. Returns per-email attached/skipped/errors
|
||
lists and triggers a single Xray restart if any target inbound was
|
||
running.
|
||
id: attach-many-existing-clients-to-many-inbounds-in-one-call-each-client-keeps-its-identity-emailuuidpasswordsubid-and-a-shared-traffic-row-all-clients-are-added-to-a-target-inbound-in-a-single-addinboundclient-call-clients-already-present-on-a-target-are-reported-under-skipped-returns-per-email-attachedskippederrors-lists-and-triggers-a-single-xray-restart-if-any-target-inbound-was-running
|
||
- content: "Mirror of bulkAttach: detach many existing clients from many inbounds
|
||
in one call. For each email, intersects the client's current inbounds
|
||
with the requested set and detaches from those only; (email, inbound)
|
||
pairs where the client is not currently attached are silently no-ops.
|
||
Emails 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
|
||
detached/skipped/errors lists and triggers a single Xray restart if
|
||
any target inbound was running."
|
||
id: mirror-of-bulkattach-detach-many-existing-clients-from-many-inbounds-in-one-call-for-each-email-intersects-the-clients-current-inbounds-with-the-requested-set-and-detaches-from-those-only-email-inbound-pairs-where-the-client-is-not-currently-attached-are-silently-no-ops-emails-not-attached-to-any-of-the-requested-inbounds-are-reported-under-skipped-client-records-are-kept-even-if-they-become-orphaned--use-bulkdel-for-full-removal-returns-per-email-detachedskippederrors-lists-and-triggers-a-single-xray-restart-if-any-target-inbound-was-running
|
||
- content: Zero up/down counters for many clients in one call. Loops the
|
||
single-reset path so each client is re-enabled across its attached
|
||
inbounds and pushed to Xray/remote nodes. Returns the count of
|
||
successfully reset clients.
|
||
id: zero-updown-counters-for-many-clients-in-one-call-loops-the-single-reset-path-so-each-client-is-re-enabled-across-its-attached-inbounds-and-pushed-to-xrayremote-nodes-returns-the-count-of-successfully-reset-clients
|
||
- content: List all client groups with their member counts. Merges persisted
|
||
groups (rows in client_groups, including empty placeholders) with the
|
||
distinct group_name values currently set on clients. Sorted
|
||
alphabetically (case-insensitive).
|
||
id: list-all-client-groups-with-their-member-counts-merges-persisted-groups-rows-in-client_groups-including-empty-placeholders-with-the-distinct-group_name-values-currently-set-on-clients-sorted-alphabetically-case-insensitive
|
||
- content: Return just the email list of clients that currently belong to the
|
||
given group. Useful for fanning a single bulk action over an entire
|
||
group without round-tripping the full client list.
|
||
id: return-just-the-email-list-of-clients-that-currently-belong-to-the-given-group-useful-for-fanning-a-single-bulk-action-over-an-entire-group-without-round-tripping-the-full-client-list
|
||
- content: Create a new empty (placeholder) group. The group becomes selectable in
|
||
client forms and the filter drawer even before any client is added to
|
||
it. Errors if a group with the same name already exists.
|
||
id: create-a-new-empty-placeholder-group-the-group-becomes-selectable-in-client-forms-and-the-filter-drawer-even-before-any-client-is-added-to-it-errors-if-a-group-with-the-same-name-already-exists
|
||
- content: Rename a group. The new name is applied to the client_groups row AND
|
||
propagated to every matching client (both clients.group_name and the
|
||
client entry inside every owning inbound's settings JSON) in a single
|
||
transaction. Returns the number of clients whose label was updated.
|
||
id: rename-a-group-the-new-name-is-applied-to-the-client_groups-row-and-propagated-to-every-matching-client-both-clientsgroup_name-and-the-client-entry-inside-every-owning-inbounds-settings-json-in-a-single-transaction-returns-the-number-of-clients-whose-label-was-updated
|
||
- content: Remove a group. Deletes the client_groups row and clears the group
|
||
label from every matching client (both clients.group_name and the
|
||
inbound settings JSON). The clients themselves are NOT deleted — use
|
||
/bulkDel after filtering by group for that. Returns the count of
|
||
clients whose label was cleared.
|
||
id: remove-a-group-deletes-the-client_groups-row-and-clears-the-group-label-from-every-matching-client-both-clientsgroup_name-and-the-inbound-settings-json-the-clients-themselves-are-not-deleted--use-bulkdel-after-filtering-by-group-for-that-returns-the-count-of-clients-whose-label-was-cleared
|
||
- content: Reset only the group-level traffic counter shown on the groups page.
|
||
Snapshots the current up/down sum of the group's members as a baseline
|
||
so the group total reads zero, while leaving each client's own
|
||
counters (and their quotas) untouched. No Xray restart is triggered.
|
||
Creates the client_groups row if the group exists only as a derived
|
||
label.
|
||
id: reset-only-the-group-level-traffic-counter-shown-on-the-groups-page-snapshots-the-current-updown-sum-of-the-groups-members-as-a-baseline-so-the-group-total-reads-zero-while-leaving-each-clients-own-counters-and-their-quotas-untouched-no-xray-restart-is-triggered-creates-the-client_groups-row-if-the-group-exists-only-as-a-derived-label
|
||
- content: Zero out a single client’s up/down counters. Re-enables the client
|
||
across every attached inbound and pushes the change to Xray (or the
|
||
remote node) so depleted users can connect again immediately.
|
||
id: zero-out-a-single-clients-updown-counters-re-enables-the-client-across-every-attached-inbound-and-pushes-the-change-to-xray-or-the-remote-node-so-depleted-users-can-connect-again-immediately
|
||
- content: Manually adjust a client’s upload + download counters. Useful for
|
||
migrations from external accounting systems.
|
||
id: manually-adjust-a-clients-upload--download-counters-useful-for-migrations-from-external-accounting-systems
|
||
- content: List source IPs that have connected with the given client’s
|
||
credentials. Returns an array of "ip (timestamp)" strings.
|
||
id: list-source-ips-that-have-connected-with-the-given-clients-credentials-returns-an-array-of-ip-timestamp-strings
|
||
- content: Reset the recorded IP list for a client.
|
||
id: reset-the-recorded-ip-list-for-a-client
|
||
- content: List registered HWID devices for a client. Hashes are not exposed.
|
||
id: list-registered-hwid-devices-for-a-client-hashes-are-not-exposed
|
||
- content: Clear all registered HWID devices for a client so new devices can
|
||
register again.
|
||
id: clear-all-registered-hwid-devices-for-a-client-so-new-devices-can-register-again
|
||
- content: Remove a single registered HWID device by its id, freeing one slot
|
||
under the HWID limit.
|
||
id: remove-a-single-registered-hwid-device-by-its-id-freeing-one-slot-under-the-hwid-limit
|
||
- content: List the emails of currently connected clients (last seen within the
|
||
heartbeat window), deduped across every node.
|
||
id: list-the-emails-of-currently-connected-clients-last-seen-within-the-heartbeat-window-deduped-across-every-node
|
||
- content: Online client emails grouped by the panelGuid of the node that
|
||
physically hosts each client. The local panel uses its own GUID; each
|
||
node (at any depth in a chain) uses its GUID. Lets the inbounds page
|
||
attribute online status to the real node instead of the intermediate
|
||
one it syncs through.
|
||
id: online-client-emails-grouped-by-the-panelguid-of-the-node-that-physically-hosts-each-client-the-local-panel-uses-its-own-guid-each-node-at-any-depth-in-a-chain-uses-its-guid-lets-the-inbounds-page-attribute-online-status-to-the-real-node-instead-of-the-intermediate-one-it-syncs-through
|
||
- content: Per-client source IPs grouped by the panelGuid of the node that
|
||
observed them. Lets the central panel attribute and enforce per-client
|
||
IP limits using the real visitor IPs each node sees, instead of the
|
||
address of the intermediate panel it syncs through.
|
||
id: per-client-source-ips-grouped-by-the-panelguid-of-the-node-that-observed-them-lets-the-central-panel-attribute-and-enforce-per-client-ip-limits-using-the-real-visitor-ips-each-node-sees-instead-of-the-address-of-the-intermediate-panel-it-syncs-through
|
||
- content: Inbound tags that carried traffic within the heartbeat window, grouped
|
||
by the hosting node's panelGuid. Pairs with onlinesByGuid so the
|
||
inbounds page only marks a multi-inbound client online on the inbounds
|
||
it actually used. Nodes that do not report per-inbound activity are
|
||
absent.
|
||
id: inbound-tags-that-carried-traffic-within-the-heartbeat-window-grouped-by-the-hosting-nodes-panelguid-pairs-with-onlinesbyguid-so-the-inbounds-page-only-marks-a-multi-inbound-client-online-on-the-inbounds-it-actually-used-nodes-that-do-not-report-per-inbound-activity-are-absent
|
||
- content: Map of client email → last-seen unix timestamp.
|
||
id: map-of-client-email--last-seen-unix-timestamp
|
||
- content: Traffic counters for a client identified by email.
|
||
id: traffic-counters-for-a-client-identified-by-email
|
||
- content: Return every protocol URL (vless://, vmess://, trojan://, ss://,
|
||
hysteria://, hy2://) for clients matching the subscription ID. Same
|
||
result set as /sub/<subId>, but as a JSON array — no base64. When an
|
||
inbound has streamSettings.externalProxy set, one URL is emitted per
|
||
external proxy. Empty array when the subId has no enabled clients.
|
||
id: return-every-protocol-url-vless-vmess-trojan-ss-hysteria-hy2-for-clients-matching-the-subscription-id-same-result-set-as-subsubid-but-as-a-json-array--no-base64-when-an-inbound-has-streamsettingsexternalproxy-set-one-url-is-emitted-per-external-proxy-empty-array-when-the-subid-has-no-enabled-clients
|
||
- content: 'Return every URL for one client across all attached inbounds — the
|
||
same strings the Copy URL button copies in the panel UI. Supported
|
||
protocols: vmess, vless, trojan, shadowsocks, hysteria. If
|
||
streamSettings.externalProxy is set, returns one URL per external
|
||
proxy. Protocols without a URL form (socks, http, mixed, wireguard,
|
||
dokodemo, tunnel) contribute nothing.'
|
||
id: return-every-url-for-one-client-across-all-attached-inbounds--the-same-strings-the-copy-url-button-copies-in-the-panel-ui-supported-protocols-vmess-vless-trojan-shadowsocks-hysteria-if-streamsettingsexternalproxy-is-set-returns-one-url-per-external-proxy-protocols-without-a-url-form-socks-http-mixed-wireguard-dokodemo-tunnel-contribute-nothing
|
||
contents:
|
||
- content: >-
|
||
Fields the server fills in when they are omitted — a valid value sent
|
||
by the caller is never overwritten. Re-adding an email that already
|
||
exists, with its stored `subId`, reuses the stored `id`, `password`,
|
||
`auth` and `secret` instead of minting new ones, so the identity stays
|
||
in sync across its inbounds.
|
||
|
||
|
||
- **VLESS / VMess** — `id`, a fresh UUID
|
||
|
||
- **Trojan** — `password`
|
||
|
||
- **Shadowsocks** — `password`. On a `2022-blake3-*` inbound a
|
||
supplied password that does not base64-decode to the key length of the
|
||
cipher (16 or 32 bytes) is replaced by a generated key and the call
|
||
still succeeds, so read the client back if you did not let the server
|
||
pick. Legacy ciphers keep any non-empty password
|
||
|
||
- **Hysteria** — `auth`
|
||
|
||
- **mtproto** — `secret`, a FakeTLS secret derived from the fronting
|
||
domain of the inbound, or from `www.cloudflare.com` when it has none
|
||
|
||
- **WireGuard** — `privateKey` and `publicKey` when both are blank, or
|
||
`publicKey` alone when only a `privateKey` was sent, plus
|
||
`allowedIPs`: one free `/32` taken from the /24 the existing peers of
|
||
that inbound already sit in, or from `10.0.0.0/24` when it has none
|
||
|
||
|
||
Accepted on the same body but never generated: `preSharedKey` and
|
||
`keepAlive` (WireGuard), `adTag` (mtproto).
|
||
|
||
|
||
WireGuard is the only one of these that can fail. Allocation widens
|
||
the search to the containing /16 before giving up with `wireguard: no
|
||
free address available in <scope>`, and an `allowedIPs` supplied by
|
||
the caller is validated instead of allocated: `wireguard: allowedIPs
|
||
entry already used by another client: <address>` when a different
|
||
client of that same inbound already holds it. The check is per
|
||
inbound, so the same address on two different inbounds is accepted.
|
||
The same validation runs on POST /panel/api/clients/{email}/attach,
|
||
where a client that already carries an address brings it along.
|
||
heading: create-a-new-client-and-attach-it-to-one-or-more-inbounds-in-a-single-call-body-is-json-per-protocol-secrets-are-generated-server-side-when-omitted-so-callers-can-send-only-the-universal-fields
|
||
- content: 'A WireGuard client brings its stored `allowedIPs` into the new inbound
|
||
instead of being given a fresh address, so the call fails with
|
||
`wireguard: allowedIPs entry already used by another client:
|
||
<address>` when a different client of the target inbound already holds
|
||
it. Free the address on that inbound first — see POST
|
||
/panel/api/clients/add for the full rule.'
|
||
heading: attach-an-existing-client-to-one-or-more-additional-inbounds-body-is-json
|
||
---
|
||
|
||
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
|
||
|
||
export default function Layout(props) {
|
||
const { APIPage, OpenAPIPage } = props.components ?? {};
|
||
// "APIPage" is the old name from v10, this allows both for backward compatibility
|
||
const Comp = OpenAPIPage ?? APIPage;
|
||
return (
|
||
<>
|
||
{props.children}
|
||
<Comp document="./public/openapi.json" webhooks={[]} operations={[{"path":"/panel/api/clients/list","method":"get"},{"path":"/panel/api/clients/list/paged","method":"get"},{"path":"/panel/api/clients/get/{email}","method":"get"},{"path":"/panel/api/clients/get/tgId/{tgId}","method":"get"},{"path":"/panel/api/clients/add","method":"post"},{"path":"/panel/api/clients/update/{email}","method":"post"},{"path":"/panel/api/clients/del/{email}","method":"post"},{"path":"/panel/api/clients/{email}/attach","method":"post"},{"path":"/panel/api/clients/{email}/detach","method":"post"},{"path":"/panel/api/clients/{email}/externalLinks","method":"post"},{"path":"/panel/api/clients/resetAllTraffics","method":"post"},{"path":"/panel/api/clients/delDepleted","method":"post"},{"path":"/panel/api/clients/delOrphans","method":"post"},{"path":"/panel/api/clients/export","method":"get"},{"path":"/panel/api/clients/import","method":"post"},{"path":"/panel/api/clients/bulkAdjust","method":"post"},{"path":"/panel/api/clients/bulkEnable","method":"post"},{"path":"/panel/api/clients/bulkDisable","method":"post"},{"path":"/panel/api/clients/bulkDel","method":"post"},{"path":"/panel/api/clients/bulkCreate","method":"post"},{"path":"/panel/api/clients/groups/bulkAdd","method":"post"},{"path":"/panel/api/clients/groups/bulkRemove","method":"post"},{"path":"/panel/api/clients/bulkAttach","method":"post"},{"path":"/panel/api/clients/bulkDetach","method":"post"},{"path":"/panel/api/clients/bulkResetTraffic","method":"post"},{"path":"/panel/api/clients/groups","method":"get"},{"path":"/panel/api/clients/groups/{name}/emails","method":"get"},{"path":"/panel/api/clients/groups/create","method":"post"},{"path":"/panel/api/clients/groups/rename","method":"post"},{"path":"/panel/api/clients/groups/delete","method":"post"},{"path":"/panel/api/clients/groups/resetTraffic","method":"post"},{"path":"/panel/api/clients/resetTraffic/{email}","method":"post"},{"path":"/panel/api/clients/updateTraffic/{email}","method":"post"},{"path":"/panel/api/clients/ips/{email}","method":"post"},{"path":"/panel/api/clients/clearIps/{email}","method":"post"},{"path":"/panel/api/clients/hwids/{email}","method":"post"},{"path":"/panel/api/clients/hwids/{email}","method":"delete"},{"path":"/panel/api/clients/hwids/{email}/{id}","method":"delete"},{"path":"/panel/api/clients/onlines","method":"post"},{"path":"/panel/api/clients/onlinesByGuid","method":"post"},{"path":"/panel/api/clients/clientIpsByGuid","method":"post"},{"path":"/panel/api/clients/activeInbounds","method":"post"},{"path":"/panel/api/clients/lastOnline","method":"post"},{"path":"/panel/api/clients/traffic/{email}","method":"get"},{"path":"/panel/api/clients/subLinks/{subId}","method":"get"},{"path":"/panel/api/clients/links/{email}","method":"get"}]} showTitle />
|
||
</>
|
||
);
|
||
} |