{"owner":"MHSanaei","repo":"3x-ui","hasSkills":true,"totalSkillsCount":3,"totalTokensCount":15155,"categories":["claude-rule","project-spec"],"hasMcp":false,"mcpConfig":null,"found":["frontend/CLAUDE.md","CLAUDE.md","docs/architecture.md"],"skills":{"frontend/CLAUDE.md":"# frontend/CLAUDE.md\n\nFrontend agent guide. Full detail: `frontend/README.md` and the root\n`CONTRIBUTING.md` (\"Working on the frontend\"). This is the short version.\n\n## What this is\nReact 19 + Ant Design 6 + Vite 8 + TypeScript. The Vite config is\n`vite.config.js` (plain JS). Three bundles, each emitted into\n`internal/web/dist/` and embedded into the Go binary:\n- `index.html` — admin panel SPA (entry `src/main.tsx`; react-router under\n  `/panel`, lazy routes).\n- `login.html` — login + 2FA (`src/entries/login.tsx`).\n- `subpage.html` — public subscription viewer (`src/entries/subpage.tsx`).\nThe `@` import alias maps to `src/`.\n\n## Data flow\n- Server state via TanStack Query (`src/api/`, keys in `src/api/queryKeys.ts`);\n  invalidate on mutation. WebSocket pushes feed the cache\n  (`src/api/websocketBridge.ts`).\n- Local UI state in the page (`useState`); shared concerns via `src/hooks/`.\n  Extend an existing hook before adding a global.\n- Zod (`src/schemas/`) is the single source of truth for the xray config model.\n  Infer types with `z.infer`. Go-side types are mirrored into `src/generated/`\n  by `npm run gen:zod` (`go run ./tools/openapigen`) — do not hand-edit that\n  folder (every file is marked `DO NOT EDIT`).\n- xray domain logic (links, defaults, form<->wire adapters) is pure functions in\n  `src/lib/xray/`. HTTP goes through `HttpUtil` in `src/utils/index.ts`.\n\n## Rules\n- Ant Design 6 for components; no Tailwind/shadcn (a migration was rolled back).\n  Form *state* runs on React Hook Form (`src/components/form/rhf/`), not Ant\n  Design's `Form` store.\n- Function components + hooks only; no class components.\n- Comments in committed TS/TSX: 2 lines MAX per comment block, spent on the\n  *why* a name cannot hold (same rule as root CLAUDE.md). HTML comments are fine.\n- TS strict; oxlint's `typescript/no-explicit-any` is an error. Build forms with `useZodForm` +\n  `FormField` from `@/components/form/rhf` (wrap the tree in `FormProvider`);\n  validate through the `zodResolver` or per-field\n  `rules={{ validate: rhfZodValidate(Schema.shape.field) }}` — messages are Zod\n  issue keys resolved via `t()`, never inline `z.string()`. AntD `<Form>` stays\n  only as a layout wrapper. Complex shared config editors (FinalMask / Sniffing /\n  Sockopt) remain AntD-`Form` islands wrapped as value/onChange adapters in\n  `src/lib/xray/forms/fields/`, bound via a `Controller`.\n- New `g.POST`/`g.GET` route => add it to `src/pages/api-docs/endpoints.ts`,\n  then `npm run gen`.\n- i18n strings live in `internal/web/translation/<locale>.json`, NOT under\n  `frontend/`, and are shared with the Go backend. A new English key must be\n  added to every locale. Interpolation here uses single braces `{var}`, not the\n  i18next default `{{var}}`.\n- Persian/Arabic (RTL) users are first-class — isolate code identifiers on their\n  own line when writing Persian text in labels/toasts.\n- Vite is pinned to an exact version (no `^`) — bump deliberately, then verify\n  `npm run dev` AND `npm run build`.\n\n## Adding a panel route\n1. `src/pages/<page>/<Page>.tsx` (kebab folder, PascalCase component).\n2. Register in `src/routes.tsx` under `/panel` (lazy import).\n3. Add a sidebar link in `src/layouts/AppSidebar.tsx` if it needs nav.\nOnly standalone bundles (login/subpage) need a new `.html` + `src/entries/*` +\n`rollupOptions.input` (in `vite.config.js`) + a Go controller route.\n\n## Commands\n- `npm run dev` (HMR on :5173, proxies to the Go panel on :2053 — start Go first).\n- `npm run typecheck` / `npm run lint` / `npm run test` / `npm run build`.\n- `npm run gen` = `gen:zod` (Go → `src/generated/`) + `gen:api`\n  (`build-openapi.mjs` → `public/openapi.json`).\n- `npm run storybook` (workbench on :6006) / `npm run build-storybook` (CI\n  compile-checks every story). Reusable `src/components/` get a co-located\n  `<Component>.stories.tsx` with `tags: ['autodocs']`; document props via\n  `argTypes` / `parameters.docs` string metadata, never JSDoc.\n- After `npm run build`, RESTART `go run .` (see the XUI_DEBUG gotcha in root\n  CLAUDE.md) before checking the panel.\n","CLAUDE.md":"# CLAUDE.md\n\nOperational guide for AI agents working in this repo. Long-form human docs:\n`CONTRIBUTING.md` (setup, testing philosophy) and `frontend/README.md`.\nRead those before large changes. This file is the short, must-follow version.\nFor a deep navigation map (request lifecycle, cron-job table, symptom → file\nindex, layering rules), read `docs/architecture.md` on demand — do not guess\nfile locations when it can answer in one hop.\n\n## Stack\n- Backend: Go 1.27 (`module github.com/mhsanaei/3x-ui/v3`), Gin, GORM.\n  Runs Xray-core as a managed child process (`internal/xray/process.go`) and\n  imports `github.com/xtls/xray-core` for config types + gRPC stats/handler/router\n  API. MTProto inbounds run a second managed child — the `mtg-multi` binary\n  (a multi-secret mtg fork — NOT a Go dependency; its prebuilt release binary is\n  fetched at image/release build time by `DockerInit.sh` + `release.yml`,\n  panel-side code in `internal/mtproto/`) — outside Xray, one process per inbound\n  serving each\n  client's FakeTLS secret via the fork's `[secrets]` section (plus per-client\n  ad-tags via `[secret-ad-tags]` and per-client data quota / expiry via\n  `[secret-limits]`, mapped from the client's `totalGB`/`expiryTime`). Client,\n  ad-tag and quota/expiry edits are hot-applied through the fork's management API\n  (`PUT /secrets`, bearer-token guarded) so connections survive; the manager\n  falls back to a process restart on older binaries. A client's panel-side\n  traffic reset also calls `POST /secrets/{name}/reset-quota` so a renewed client\n  is not re-blocked by the sidecar's quota counter.\n- Storage: SQLite by default (`/etc/x-ui/x-ui.db` on Linux; the executable dir on\n  Windows), PostgreSQL optional (`XUI_DB_TYPE` / `XUI_DB_DSN`). The CGo SQLite\n  driver (`mattn/go-sqlite3`) needs a C compiler — `CGO_ENABLED=0` builds fail.\n- Frontend: React 19 + Ant Design 6 + Vite 8 + TypeScript in `frontend/`,\n  built into `internal/web/dist/` (gitignored) and embedded via `embed.FS`.\n\n## Repo map\n- `main.go` — entry point + `x-ui` CLI (run, migrate, migrate-db, encrypt-tokens, setting, cert).\n- `internal/config/` — env parsing (XUI_DEBUG, XUI_LOG_LEVEL, XUI_LOG_FOLDER,\n  XUI_BIN_FOLDER, XUI_SKIP_HSTS, XUI_PORT, XUI_DB_*).\n- `internal/database/` + `internal/database/model/` — GORM schema (~24 models;\n  Inbound, Client, Setting, User are the core), inbound Protocol enum,\n  AutoMigrate + hand-written migrations in `db.go`.\n- `internal/xray/` — Xray child-process lifecycle, config generation, gRPC API.\n- `internal/xray/geodata/` — streaming geosite/geoip `.dat` reader (cached\n  category index + paged entries) and `geosite:`/`geoip:`/`ext:` token parsing.\n- `internal/mtproto/` — MTProto inbounds via the bundled `mtg-multi` binary.\n- `internal/tuic/` — TUIC v5 inbounds: `tuic-server` sidecar supervisor, native Go UDP relay traffic metering.\n- `internal/amneziawg/` — AmneziaWG protocol shape: instance/peer derivation\n  from an inbound, 3.1 obfuscation param generation + validation, port-forward\n  spec parsing.\n- `internal/amneziawgnet/` — embedded AmneziaWG runtime: amneziawg-go device\n  over a gVisor userspace netstack, per-inbound reconcile manager, TCP/UDP\n  relay into a loopback per-peer-auth SOCKS5 Xray inbound, port-forward\n  listeners, per-peer IPv6 egress aliases.\n- `internal/pia/` — PIA WireGuard protocol client (auth, signed server list, `/addKey`).\n- `internal/sub/` — subscription server (raw / JSON / Clash).\n- `internal/eventbus/` — in-process pub/sub (outbound/node health, xray.crash,\n  cpu.high, memory.high, login.attempt).\n- `internal/logger/`, `internal/util/` (link, crypto, sys, ldap, …),\n  `internal/tunnelmonitor/` — shared infrastructure.\n- `internal/web/` — Gin server (embeds `dist/` + `translation/`).\n  - `controller/` — panel + REST API handlers; OpenAPI at /panel/api/openapi.json.\n  - `service/` — business logic (InboundService, SettingService, XrayService,\n    node sync); subpackages tgbot/, discord/, email/, outbound/, panel/, integration/.\n  - `job/` — 19 cron jobs (traffic, fail2ban IP-limit, node heartbeat/sync, LDAP,\n    CPU/memory watchdogs, …); full table in `docs/architecture.md` §5.4.\n  - `middleware/`, `entity/`, `global/`, `session/` (CSRF), `network/`,\n    `runtime/` (master/sub-node over mTLS), `websocket/`.\n  - `locale/` + `translation/` — i18n, 13 embedded locale JSON files.\n- `frontend/` — React + TS source (see `frontend/CLAUDE.md`).\n- `tools/openapigen/` — Go generator that emits frontend types + Zod/JSON schemas\n  into `frontend/src/generated/` from Go structs. The OpenAPI doc itself\n  (`frontend/public/openapi.json`) is assembled from those + `endpoints.ts` by\n  `frontend/scripts/build-openapi.mjs`.\n- `docs/` — separate Next.js/Fumadocs site (pnpm, own CI in `docs-ci.yml`,\n  outside `make verify`). Holds a THIRD independent implementation of\n  link/subscription generation in `docs/lib/xray/` — check it whenever\n  share-link or install-command output changes.\n\n## Hard rules (non-negotiable)\n- Correct fix over small fix. Find the root cause and fix it the right way, however\n  much code that takes. Size the change by what the correct fix needs, never by\n  line count: when the right fix spans many files, or needs a migration, a shared\n  helper or a new abstraction, write it. A guard that hides the symptom while the\n  cause survives is the wrong fix, however small. Two limits remain:\n  - Everything added must be something the correct fix needs. No speculative\n    knobs, unused extension points or \"while I was here\" rewrites. Unrelated\n    refactors and cleanups go in their own commit.\n  - Stop and ask only when the right fix needs a decision the code cannot answer:\n    a deliberate user-visible behaviour change, or two sound designs with a real\n    trade-off. Ask with a recommendation. Size alone is never a reason to stop,\n    defer or ship a smaller patch.\n- Comments in committed Go/TS: 2 lines MAX per comment block. Make the name\n  carry the meaning first and rename rather than annotate; spend the 2 lines on\n  the *why* a name cannot hold — an invariant, an issue number, a non-obvious\n  constraint. Exempt: `//go:build`, `//go:generate`, and other directives.\n  HTML `<!-- -->` is fine. (A linter cannot enforce this — you must.)\n- New `g.POST`/`g.GET` in `internal/web/controller/` REQUIRES a matching entry\n  in `frontend/src/pages/api-docs/endpoints.ts`, then `make gen` (or\n  `cd frontend && npm run gen`). Hand-maintained but pinned both ways by\n  `TestRouteRegistryContract` (`internal/web/routes_contract_test.go`): a missing\n  OR stale entry fails `make test-go`. Scope: `/panel/api/*` + a few session\n  routes; sub-server routes are exempt.\n- Response examples come from Go struct `example:` tags via `tools/openapigen` —\n  never hand-write them. A new struct must be added to openapigen's `StructAllow`\n  allowlist (`tools/openapigen/main.go`) or it is silently omitted from\n  schemas/examples (and `build-openapi.mjs` then fails on the missing schema).\n- A new or renamed endpoint has a FOURTH step nothing checks: copy\n  `frontend/public/openapi.json` → `docs/public/openapi.json`, then\n  `cd docs && pnpm gen:api` to refresh the MDX under\n  `docs/content/docs/en/reference/api/`. `docs-ci.yml` fires only on `docs/**`.\n- A new English i18n key goes in EVERY locale JSON in `internal/web/translation/`\n  (13 files) AND must be referenced from `frontend/src` or Go in the SAME commit —\n  `frontend/src/test/i18n-dead-keys.test.ts` fails both ways. It is a frontend\n  test, so run `npm test`, not just `make test-go`. At runtime the frontend falls\n  back to en-US; Go (`internal/web/locale/`) returns \"\" for an unknown key.\n- DB / model changes require a migration in `internal/database/db.go`.\n- Every state-changing inbound/client op dispatches through `runtime.Runtime`\n  (`internal/web/runtime/`) — never straight to `internal/xray/api.go`, never from\n  a controller or cron job. A direct call passes every local test and silently\n  breaks every multi-node deployment. Other layering rules: `docs/architecture.md` §8.\n- Conventional commits: `type(area): short imperative summary`, then a body\n  explaining the why. Types in use: `fix`, `feat`, `chore`, `refactor`, `perf`,\n  `docs`, `style`.\n\n## Tests: TDD, and only tests that can fail (Go and frontend)\n- Work red → green → refactor.\n  - Bug: turn the reproduction into a test first, and watch it fail for the\n    reported reason.\n  - Feature: write the test for the first behaviour before writing its code.\n  - Then write the code that makes it pass, and refactor with the suite green.\n\n  If a test was written after the code, prove it anyway: revert the code, watch\n  the test go red, then restore. A test that passes either way is worse than no\n  test. It certifies nothing, and then gets cited as proof the fix works.\n- Every test must name the failure it catches. When no test can reach a change\n  (workflow YAML, pure wiring, layout), say so and name the command that\n  demonstrates it. Never write a stand-in test.\n- Fake tests are forbidden. Delete any you write or meet in the code you touch:\n  - tests of a getter, a constant, a rename, a pure map lookup, or an input the\n    function can never receive;\n  - tests that restate the implementation, such as recomputing the expected\n    value with the same formula or asserting that a mock was called exactly the\n    way the code calls it;\n  - mocking the unit under test, or mocking so much around it that the real\n    code path never runs;\n  - assertions too weak to fail: `err != nil`, `len > 0`, `toBeDefined()`, or\n    `not.toThrow()` alone;\n  - golden files or snapshots regenerated to match whatever the code now outputs;\n  - extra cases that exercise no distinct branch, and tests written to raise\n    coverage.\n\n  One real test that drives the bug through the actual code path beats five\n  that restate the code.\n\n## Go conventions\n- Stdlib `testing` only (no testify). Table-driven, `t.Run` subtests,\n  `t.Helper()` on helpers. Assert the exact value / typed error / emitted\n  string, never just `err != nil`. Prefer real deps over mocks: throwaway DB via\n  `dbtest.InitDB(t, filepath.Join(t.TempDir(), \"x-ui.db\"))`\n  (`internal/database/dbtest`: copies a once-migrated template and registers\n  `CloseDB` cleanup; a fresh `database.InitDB` costs ~7x more, ~850ms under\n  `-race`); `httptest` for HTTP. Keep `database.InitDB` for reopening a file or\n  migrating a hand-built legacy DB. `internal/sub`'s `initSubDB(t)` is the template.\n- Code must pass `golangci-lint run` (gofumpt + goimports formatting): `make lint`.\n- Postgres, xray-gRPC-e2e, master+node and scale tests `t.Skip` unless\n  `XUI_TEST_PG_DSN`, `XUI_DB_TYPE`+`XUI_DB_DSN`, `XRAY_E2E_BINARY`,\n  `XUI_NODE_E2E_BINARY` or `XUI_SCALE_TEST` is set — a green `go test ./...`\n  does not mean those paths ran.\n\n## Frontend conventions (summary; full version in frontend/CLAUDE.md)\n- Ant Design 6 only — no Tailwind/shadcn. Targeted tweaks, not rewrites.\n- TS strict; `@typescript-eslint/no-explicit-any` is an error. Zod schemas in\n  `src/schemas/` are the source of truth; infer types with `z.infer`, never\n  hand-write. Do not edit `src/generated/`.\n- Node 26 (`.nvmrc`) — `make gen` imports `.ts` directly and needs its type\n  stripping; Node 22 dies with `ERR_UNKNOWN_FILE_EXTENSION`. `npm test` includes\n  a headless-Chromium Storybook project, so run\n  `npx playwright install --with-deps chromium` once or `make verify` fails.\n- Editing `frontend/src` does NOT change what users see until the Vite build is\n  regenerated into `internal/web/dist/`. In `XUI_DEBUG=true`, HTML is served from\n  the frozen embedded FS but JS/CSS off disk — after `npm run build` you MUST\n  restart `go run .` or you get a blank page with 404s.\n- After touching share-link logic (`src/lib/xray/`), run `npm run test` (golden\n  fixtures); regenerate snapshots (`npx vitest run -u`) only for intentional\n  output changes, never to make a red test green.\n\n## Build, test, verify\nA fresh clone has no `internal/web/dist/`, so a bare `go build ./...` dies with\n`pattern all:dist: no matching files found` while ~35 other packages pass — it\nreads as a broken repo, not a missing step. Run `make dist-stub` once; every\n`make` Go target already depends on it, which is why `make test-go` beats\n`go test ./...`. Run `make help` for all targets. The local gate:\n\n    make verify   # gen-check + lint + typecheck + test + build + build-storybook\n\nThat is the *fast* gate, not all of CI. `ci.yml` also runs `make race`,\n`make vulncheck`, a live-Postgres job (where a SKIP counts as a failure),\n`make node-e2e` (a real master and node panel, `internal/nodee2e/`) and a\n30s fuzz smoke on `FuzzParseLink`/`FuzzDecodeCertPin` — run those locally when\nyou touch DB/dialect, node sync or parser code. Node sync has two layers: every\n`runtime.Remote` call gets a cell in `internal/web/node_contract_test.go` (fast,\nin `make test-go`; a method without one fails it), and a multi-tick flow (cron,\nadopt, node down) gets one in `internal/nodee2e/node_sync_test.go`.\n\nCommon targets: `make gen` (regenerate Zod/OpenAPI), `make lint` (Go + frontend),\n`make test` (Go `-shuffle=on` + frontend), `make race`, `make build`. See `Makefile`.\n\n## Definition of done (before opening a PR)\n1. `make verify` passes — its `gen-check` already runs `make gen` and fails on a\n   dirty `frontend/src/generated` / `frontend/public/openapi.json`.\n2. Diff is focused; refactors are separate from feature work.\n","docs/architecture.md":"# 3x-ui — Architecture & Code Map\n\n> Navigation map for contributors and AI coding agents (referenced from `CLAUDE.md`).\n> Goal: jump to the right file in one hop instead of grepping the whole tree.\n> Tracks the `main` branch — paths reflect the latest changes, so verify against the live\n> tree rather than a pinned release (Go module `github.com/mhsanaei/3x-ui/v3`).\n>\n> **How to use this file:** read \"Mental model\" + \"Request lifecycle\" first, then\n> use the **Symptom → File index** to locate work. Respect the **Layering rules**\n> when adding code. Verify with the commands in **Build / Test / Lint**.\n\n---\n\n## 1. Mental model (the 30-second version)\n\n3x-ui is a **web control panel for [Xray-core](https://github.com/XTLS/Xray-core)**. The Go\nbackend is the source of truth: it stores inbounds/clients/settings in a DB, renders an\nXray JSON config from that state, supervises the Xray child process, and exposes a REST +\nWebSocket API. A React SPA (built by Vite, embedded into the Go binary) is the UI. A second,\nseparate HTTP server serves **subscription links** to end users.\n\nThe panel supervises **managed child processes**: Xray-core itself and — when MTProto\ninbounds exist — a dedicated child proxy binary:\n\n- **`mtg-multi` for MTProto inbounds** (`github.com/mhsanaei/mtg-multi`, a multi-secret fork\n  built from source; `internal/mtproto/`): One process per inbound serves every attached\n  client's FakeTLS secret through the fork's `[secrets]` section, plus optional per-client\n  sponsored-channel ad-tags via `[secret-ad-tags]`. A client or ad-tag edit is hot-applied via\n  the fork's management API (`PUT /secrets`, guarded by a per-process bearer token), with a\n  process restart as the fallback on older binaries.\n\nIn contrast, **AmneziaWG** (`internal/amneziawgnet/`) and **TUIC v5** (`internal/tuic/`) run as\n**in-process native Go servers** without external child processes, bridging client traffic into\nXray-core via loopback SOCKS5 relays.\n\nServers and processes, all launched from `main.go`:\n\n| Server / process | Package                           | Purpose                                                            | Default port      |\n| ---------------- | --------------------------------- | ------------------------------------------------------------------ | ----------------- |\n| **Panel**        | `internal/web`                    | Admin REST/WS API + serves the embedded SPA                        | 2053              |\n| **Subscription** | `internal/sub`                    | Public endpoint that hands out client configs (raw / JSON / Clash) | `subPort` setting |\n| **Xray-core**    | supervised via `internal/xray`    | The actual proxy engine; a child process, not Go code              | `inbounds[].port` |\n| **mtg-multi**    | supervised via `internal/mtproto` | MTProto proxy child process for MTProto inbounds (multi-secret)    | per inbound       |\n\nTwo key ideas that explain most of the complexity:\n\n1. **The DB → Xray config pipeline.** Inbounds/clients live in the DB. On every change the\n   backend regenerates the Xray config and applies it — preferring a _hot diff_ (live gRPC\n   API mutation) over a full process restart. See §5.1.\n2. **The Runtime abstraction (multi-node).** A panel can manage remote \"nodes\" (other 3x-ui\n   instances). Every state-changing inbound/client operation is dispatched through a\n   `runtime.Runtime` interface that is either **`Local`** (this box's Xray gRPC API) or\n   **`Remote`** (HTTPS call to a child node, with `verify`/`skip`/`pin`/`mtls` TLS modes).\n   This is the single most important abstraction in the project. See §5.2.\n\n---\n\n## 2. Tech stack\n\n**Backend (Go 1.27):**\n\n- Web framework: **Gin** (`gin-gonic/gin`) + sessions (cookie store), gzip.\n- ORM: **GORM** with **SQLite** (default) or **PostgreSQL** (`XUI_DB_TYPE=postgres`).\n- Scheduler: **robfig/cron/v3** (seconds-precision) for all background jobs.\n- Xray: **xtls/xray-core** vendored as a library; the panel talks to the running core over\n  its **gRPC API** and also shells out to manage the process.\n- Bots: Telegram bot (**mymmrac/telego**), Discord bot (Discord REST API v10 + **gorilla/websocket** Gateway v10). i18n: **nicksnyder/go-i18n**.\n- Misc: gorilla/websocket, gopsutil (system stats), go-qrcode, gotp (2FA TOTP).\n\n**Frontend (`frontend/`):**\n\n- **React 19** + **Ant Design 6** + **Vite 8** + **TypeScript**.\n- Data layer: **TanStack Query** (`@tanstack/react-query`) over the native **Fetch API**; **Zod 4** schemas.\n- Router: **react-router 8**. Charts: **uPlot** (`frontend/src/components/viz/Sparkline.tsx`). Editor: **CodeMirror 6**.\n- **Build output goes to `internal/web/dist/`** (see `vite.config.js` → `outDir`) and is\n  embedded into the Go binary with `go:embed`. Three HTML entries: `index.html` (panel SPA),\n  `login.html`, `subpage.html`. The Go server serves the SPA; there is no separate frontend\n  deployment.\n\n**Important:** the legacy Go-template UI and `web/assets/` are **gone**. All HTML/JS comes\nfrom the embedded Vite `dist/`. Don't look for `.html` templates in `internal/web`.\n\n---\n\n## 3. Request lifecycle (follow the data)\n\n### 3.1 Admin API request (e.g. \"add a client\")\n\n```\nBrowser (React, fetch)\n  → POST {basePath}/panel/api/...\n    → Gin engine (internal/web/web.go: initRouter)\n      → middleware chain: SecurityHeaders → MaxBodyBytes (10 MiB; importDB exempt)\n                          → [DomainValidator, if webDomain set] → gzip → sessions(\"3x-ui\")\n                          → base-path/cache-control context → Localizer\n                          → API routes add: ConfigEnvelope (zstd + SHA-256) → CSRF\n        → Controller (internal/web/controller/*.go)   // HTTP concerns only: bind, validate, respond\n          → Service (internal/web/service/*.go)        // business logic + transactions\n            → GORM → DB (internal/database)            // persistence\n            → runtime.Runtime dispatch                 // apply to Xray (Local) or node (Remote)\n              → Local:  internal/xray (gRPC API or config regen + restart)\n              → Remote: internal/web/runtime/remote.go → HTTPS → child node's API\n```\n\nThe controller layer is thin. **Business logic lives in services.** When something is wrong\nwith _behavior_, the bug is almost always in a service file, not a controller.\n\n### 3.2 Subscription request (end-user fetching their config)\n\n```\nEnd user → GET {subPath}/{subId}   (separate server, internal/sub)\n  → internal/sub/controller.go (routes: raw / JSON / Clash variants, feature-flagged)\n    → internal/sub/service.go (~2.5k lines — the link/config builder)\n      → reads inbounds+clients+hosts from DB, renders per-protocol share links /\n        Clash YAML / JSON (Host rows can override address/SNI/path per inbound)\n```\n\n### 3.3 Background work (cron jobs)\n\nScheduled in `internal/web/web.go` → `startTask()`. Each job is a struct in\n`internal/web/job/`. Examples: poll Xray traffic every 5s, check client IP limits every 10s,\nnode heartbeat every 5s, periodic traffic resets (hourly/daily/weekly/monthly). See §5.4.\n\n---\n\n## 4. Directory map (what lives where)\n\n```\n3x-ui/\n├── main.go                     # Entry point: CLI (run / migrate / migrate-db / setting / cert),\n│                               #   bootstrap, signal handling, restart loop\n├── go.mod / go.sum             # Go deps (module path ends in /v3)\n│\n├── internal/                   # ALL backend Go code (private packages)\n│   ├── config/                 # Env-var config: paths, DB kind/DSN, log level, version\n│   │                           #   Every XUI_* env var is read here (config.go)\n│   ├── database/\n│   │   ├── db.go               # InitDB: connect, AutoMigrate, seeders (~1.4k lines). DB hotspot.\n│   │   ├── migrate_data.go     # Data migrations (seeders/normalizers beyond AutoMigrate)\n│   │   ├── dialect.go          # SQLite vs Postgres SQL differences\n│   │   ├── dump_sqlite.go      # DB export/backup\n│   │   └── model/              # **ALL GORM models** (model.go ~1.1k lines + siblings:\n│   │                           #   node_client_traffic.go, node_client_ip.go,\n│   │                           #   client_global_traffic.go). ⭐ Start here for data shape.\n│   ├── pia/                    # PIA WireGuard protocol client (auth, signed server list, /addKey)\n│   ├── eventbus/               # In-process pub/sub (buffered channel): outbound.down|up,\n│   │                           #   xray.crash, node.down|up, cpu.high, memory.high, login.attempt\n│   ├── tunnelmonitor/          # Optional tunnel health probe (XUI_TUNNEL_HEALTH_* env vars):\n│   │                           #   HTTP probe (default Cloudflare trace); repeated failures\n│   │                           #   trigger an Xray restart hook. Independent of panel settings.\n│   ├── xray/                   # Xray-core integration (the proxy engine wrapper)\n│   │   ├── process.go          # Spawn/supervise the Xray child process (~750 lines)\n│   │   ├── api.go              # gRPC client to a running Xray (add/remove user, stats) (~800 lines)\n│   │   ├── hot_diff.go         # ⭐ Compute minimal live changes to avoid full restart (~500 lines)\n│   │   ├── config.go           # Xray config object model\n│   │   ├── inbound.go          # Inbound JSON shaping\n│   │   ├── client_traffic.go   # ClientTraffic model (persisted as client_traffics)\n│   │   ├── traffic.go          # Traffic type helpers\n│   │   ├── log_writer.go       # Pipe Xray stdout/stderr into the panel logger\n│   │   └── geodata/            # Browse geosite/geoip .dat: streaming protowire reader,\n│   │                           #   cached category index, routing-token parsing (token.go)\n│   │\n│   ├── web/                    # The panel server\n│   │   ├── web.go              # ⭐ Server bootstrap: initRouter (all routes) + startTask (all cron jobs)\n│   │   ├── controller/         # HTTP handlers (thin). One file per resource:\n│   │   │   ├── inbound.go      #   /panel/api/inbounds\n│   │   │   ├── client.go       #   /panel/api/clients (CRUD + bulk + ips + onlines)\n│   │   │   ├── group.go        #   client-group endpoints\n│   │   │   ├── node.go         #   /panel/api/nodes   (multi-node management)\n│   │   │   ├── host.go         #   /panel/api/hosts   (per-inbound subscription host overrides)\n│   │   │   ├── server.go       #   /panel/api/server  (status, xray version, certs, logs, DB import/export)\n│   │   │   ├── setting.go      #   /panel/api/setting (settings + API tokens)\n│   │   │   ├── xray_setting.go #   /panel/api/xray    (raw Xray config editor, WARP/Nord/PIA, geodata)\n│   │   │   ├── api.go          #   /panel/api gateway (token auth, envelope + CSRF wiring)\n│   │   │   ├── index.go        #   login/logout/csrf/2FA\n│   │   │   ├── spa.go          #   SPA fallback for /panel UI routes\n│   │   │   └── websocket.go    #   WS upgrade endpoint\n│   │   ├── service/            # ⭐⭐ Business logic. This is where most real work happens.\n│   │   │   ├── inbound.go              # Inbound CRUD core (~1.4k lines)\n│   │   │   ├── inbound_node.go         # ⭐ Node sync for inbounds: reconcile, traffic merge (~1.1k lines)\n│   │   │   ├── inbound_traffic.go      # Per-client traffic accounting (~1.1k lines)\n│   │   │   ├── inbound_clients.go      # Client-within-inbound operations\n│   │   │   ├── inbound_sublink.go      # Inbound-level subscription link helpers\n│   │   │   ├── inbound_migration.go    # Inbound schema/format migrations\n│   │   │   ├── client_crud.go          # Client create/read/update/delete\n│   │   │   ├── client_bulk.go          # Bulk client ops (~1.6k lines)\n│   │   │   ├── client_inbound_apply.go # ⭐ Apply client changes to runtime (Local/Remote) (~1.2k lines)\n│   │   │   ├── client_groups.go        # Client grouping\n│   │   │   ├── client_link.go          # Per-client share-link generation\n│   │   │   ├── client_external_link.go # External links attached to clients\n│   │   │   ├── client_wireguard.go     # WireGuard client specifics\n│   │   │   ├── client_paging.go        # Server-side pagination/sort/filter for client lists\n│   │   │   ├── node.go                 # ⭐ NodeService: CRUD, probe, heartbeat, dirty-tracking (~1.1k lines)\n│   │   │   ├── node_mtls.go            # Node mTLS certificate management (master side)\n│   │   │   ├── node_tree.go            # Node hierarchy / descendants\n│   │   │   ├── host.go                 # Host rows (subscription output overrides)\n│   │   │   ├── server.go               # ServerService: status, certs, xray install, DB ops (~2.2k lines)\n│   │   │   ├── setting.go              # SettingService: all panel settings + defaults (~1.3k lines)\n│   │   │   ├── setting_mtls.go         # mTLS settings (node hardening)\n│   │   │   ├── traffic_writer.go       # Batched persistence of traffic deltas to the DB\n│   │   │   ├── xray.go                 # ⭐ XrayService: config gen + restart/hot-apply (~1.2k lines)\n│   │   │   ├── xray_setting.go         # Raw Xray config persistence\n│   │   │   ├── geodata.go              # Geo database browsing + routing-token validation\n│   │   │   ├── xray_metrics.go         # Xray observability metrics\n│   │   │   ├── metric_history.go       # Historical system/xray metrics\n│   │   │   ├── reality_scan.go         # REALITY target scanner\n│   │   │   ├── url_safety.go           # Outbound URL validation (SSRF guards)\n│   │   │   ├── outbound_subscription.go# Outbound subscription (e.g. Warp/Nord provider configs)\n│   │   │   ├── port_conflict.go        # Detect inbound port collisions\n│   │   │   ├── fallback.go             # Xray fallback (SNI/ALPN routing on shared port)\n│   │   │   ├── email/                  # Email notification service (SMTP)\n│   │   │   ├── integration/            # External providers: warp.go, nord.go, pia.go\n│   │   │   ├── outbound/               # Outbound config service\n│   │   │   ├── panel/                  # Cross-cutting panel services:\n│   │   │   │   ├── panel.go            #   panel-level helpers\n│   │   │   │   ├── user.go             #   admin user auth (bcrypt)\n│   │   │   │   ├── api_token.go        #   API token CRUD (SHA-256 hashed)\n│   │   │   │   └── websocket.go        #   WS hub / push service\n│   │   │   ├── discord/                # Discord bot client, Gateway v10, and subscriber\n│   │   │   └── tgbot/                  # Telegram bot command handlers\n│   │   ├── runtime/            # ⭐⭐ The Local/Remote node abstraction (see §5.2)\n│   │   │   ├── runtime.go      #   the Runtime interface (the contract)\n│   │   │   ├── local.go        #   Local impl → this box's Xray gRPC API\n│   │   │   ├── remote.go       #   Remote impl → HTTPS calls to a child node\n│   │   │   ├── tls_client.go   #   per-node HTTP client: verify / skip / pin / mtls\n│   │   │   └── manager.go      #   RuntimeFor(nodeID) → picks Local or Remote\n│   │   ├── job/               # Cron job structs (one file per job — see §5.4)\n│   │   ├── middleware/        # Gin middleware: security.go (headers/HSTS), bodylimit.go,\n│   │   │                      #   domainValidator.go, validate.go (CSRF), config_envelope.go\n│   │   ├── global/           # Global singletons: web server + sub server handles, restart hook\n│   │   ├── network/          # Custom net listeners (e.g. proxy-protocol aware)\n│   │   ├── session/          # Session/cookie helpers\n│   │   ├── websocket/        # WS hub implementation\n│   │   ├── locale/ + translation/  # i18n middleware + 13 locale JSON catalogs\n│   │   ├── entity/           # Shared request/response DTOs\n│   │   └── dist/             # ⚠️ Vite build output, embedded via go:embed (generated — do not hand-edit)\n│   │\n│   ├── sub/                   # The subscription server (separate from panel)\n│   │   ├── sub.go             #   server bootstrap\n│   │   ├── controller.go      #   routes for raw / JSON / Clash subscription formats\n│   │   ├── service.go         # ⭐ The link/config builder (~2.5k lines — share-link logic lives here)\n│   │   ├── json_service.go    #   JSON subscription format\n│   │   ├── clash_service.go   #   Clash/Mihomo YAML format\n│   │   ├── clash_external.go  #   external Clash config integration\n│   │   ├── external_subscription.go / external_config.go  # external sub import/aggregation\n│   │   ├── host_sub.go        #   Host-row overrides applied to subscription output\n│   │   ├── endpoint.go        #   subscription endpoint configuration\n│   │   ├── vless_route.go     #   VLESS route shaping\n│   │   ├── remark_vars.go     #   remark variable expansion\n│   │   └── links.go           #   link helpers\n│   │\n│   ├── mtproto/              # Embedded MTProto (Telegram) proxy: manager.go + per-OS\n│   │                         #   process supervision + orphan cleanup\n│   ├── logger/              # App logger (op/go-logging + lumberjack rotation)\n│   └── util/                # Leaf helpers (no business logic):\n│       ├── common/          #   errors, misc\n│       ├── crypto/          #   key/cert generation (x25519, ML-KEM/ML-DSA, ECH)\n│       ├── link/            #   outbound share-link building primitives\n│       ├── wirecodec/ + wireguard/  # WireGuard codec + integration helpers\n│       └── random/, json_util/, reflect_util/, sys/, netproxy/, netsafe/, ldap/\n│\n├── frontend/                 # React SPA (built into internal/web/dist)\n│   ├── vite.config.js        # ⭐ Build config: outDir → ../internal/web/dist, dev on :5173\n│   │                         #   (strict) proxying to :2053, entries index/login/subpage.html\n│   ├── package.json          # scripts: dev / build / preview / lint / typecheck / test / gen\n│   └── src/\n│       ├── main.tsx / routes.tsx / queryClient.ts   # SPA entry, router, query client\n│       ├── entries/          # Extra HTML entry points: login.tsx, subpage.tsx\n│       ├── pages/            # ⭐ Route screens. Mirrors the panel's feature areas:\n│       │   ├── inbounds/     #   inbound list + the big inbound form (protocols/security/transport)\n│       │   ├── clients/      #   client management screens\n│       │   ├── nodes/        #   multi-node UI\n│       │   ├── hosts/        #   subscription host-override UI\n│       │   ├── xray/         #   raw Xray config UI (routing, dns, outbounds, balancers, overrides)\n│       │   ├── index/        #   dashboard/home\n│       │   └── settings/, groups/, sub/, login/, api-docs/\n│       ├── api/              # ⭐ Data layer: http-init, QueryProvider, queryKeys, websocket bridge\n│       │   └── queries/      #   TanStack Query hooks (useNodesQuery, useStatusQuery, …)\n│       ├── schemas/          # Zod schemas: protocols, forms, api, primitives\n│       ├── generated/        # ⚠️ GENERATED from Go (see §5.5): schemas.ts, types.ts, zod.ts, examples.ts\n│       ├── components/       # Reusable UI (clients/ form/ geodata/ ui/ viz/ feedback/ utility/)\n│       ├── lib/              # Frontend domain logic (xray/ inbounds/ clients/)\n│       ├── hooks/, models/, layouts/, i18n/, utils/, styles/\n│       └── test/             # Vitest + golden fixtures (config-generation snapshot tests)\n│\n├── tools/openapigen/         # ⭐ Go program that emits frontend/src/generated/* from Go types (§5.5)\n├── docs/                     # Markdown docs (this file, custom-subscription-templates.md, …)\n├── media/                    # README images\n│\n├── Dockerfile / docker-compose.yml / DockerEntrypoint.sh / DockerInit.sh   # Container build/run\n├── install.sh / update.sh / x-ui.sh                        # VPS install + management CLI\n├── x-ui.service.*  / x-ui.rc                               # systemd units (debian/rhel/arch) + rc script\n├── windows_files/                                          # Windows service support\n└── .github/workflows/        # CI: ci.yml, codeql.yml, docker.yml, release.yml,\n                              #     mutation.yml, cleanup_caches.yml, claude-pr-review.yml,\n                              #     claude-issue-analyst.yml\n```\n\n---\n\n## 5. Cross-cutting subsystems (the parts that span many files)\n\n### 5.1 DB → Xray config pipeline (config generation & application)\n\nThe panel never edits Xray's running config directly from controllers. The flow is:\n\n1. A service mutates DB state (inbound/client/setting).\n2. `XrayService` (`service/xray.go`) builds a fresh `xray.Config` from DB state\n   (`GetXrayConfig`).\n3. It tries a **hot apply** (`tryHotApply` → `xray/hot_diff.go`): diff old vs new config and\n   push only the deltas over the Xray gRPC API (add/remove inbound, add/remove user) — **no\n   process restart**, so live connections survive.\n4. If the diff isn't hot-applicable (structural change), it falls back to a **full restart**\n   of the Xray process (`xray/process.go`).\n\nRestart is debounced via an atomic \"need restart\" flag (`SetToNeedRestart` /\n`IsNeedRestartAndSetFalse`), consumed by a `@every 30s` cron task registered in `startTask()`\n— any number of mutations inside the window causes at most one restart.\n\n**Key files:** `service/xray.go` (orchestration), `xray/hot_diff.go` (the diff algorithm),\n`xray/process.go` (process lifecycle), `xray/api.go` (gRPC calls), `xray/config.go` (config model).\n\n### 5.2 Runtime abstraction — Local vs Remote (multi-node) ⭐ most important\n\nA \"node\" (`model.Node`) is another 3x-ui instance this panel controls. Every state-changing\ninbound/client operation goes through the `runtime.Runtime` interface so the _same service\ncode_ works whether the target is the local Xray or a remote node.\n\n- **Interface:** `internal/web/runtime/runtime.go` — `Name`, `AddInbound`, `DelInbound`,\n  `UpdateInbound`, `AddUser`, `RemoveUser`, `UpdateUser`, `DeleteUser`, `AddClient`,\n  `RestartXray`, `ResetClientTraffic`, `ResetInboundTraffic`, `ResetAllTraffics`.\n- **`Local`** (`local.go`): calls this box's Xray gRPC API directly.\n- **`Remote`** (`remote.go`): serializes the operation and sends it over HTTPS to the child\n  node's API.\n- **TLS modes** (`tls_client.go`, per-node `TlsVerifyMode`):\n  `verify` (system CAs, default) / `skip` (no validation) / `pin` (leaf cert SHA-256 must\n  match `PinnedCertSha256`) / `mtls` (master presents a client certificate; node cert checked\n  against system roots; API token optional). Master-side cert management:\n  `service/node_mtls.go` + `service/setting_mtls.go`.\n- **Dispatch:** `manager.go` → `Manager.RuntimeFor(nodeID *int)`; `nil` nodeID → `Local`,\n  otherwise a cached/lazy-loaded `Remote`. `InvalidateNode(id)` drops a cached remote client.\n\nBearer tokens use the narrowest scope that matches the caller's duties:\n\n| Scope        | Access                                                                 |\n| ------------ | ---------------------------------------------------------------------- |\n| `monitor`    | Read-only status, version, and metric endpoints.                       |\n| `node-sync`  | Node configuration, inbound/client synchronization, traffic, and Xray. |\n| `node-admin` | All `node-sync` access plus `POST /panel/api/server/updatePanel`.      |\n| `admin`      | Every panel API endpoint; intended for fully trusted automation.       |\n\n`node-admin` remains a node synchronization identity when inbound rules are applied. This\npreserves the node-specific validation path while keeping software-update authority separate\nfrom ordinary `node-sync` credentials. On an mTLS connection, only a valid `node-admin` bearer\nelevates the request; other bearer scopes retain the historical mTLS `node-sync` behavior.\nExisting tokens retain their stored scope, and legacy tokens without one continue to migrate to\n`admin`.\n\n**Node identity & attribution (the hard part).** Inbounds carry a `NodeID` _and_ an\n`OriginNodeGuid`. Because inbounds can be pushed across hops, the panel attributes traffic and\nonline clients back to the originating panel using **stable GUIDs** rather than local IDs.\nRelevant logic: `service/inbound_node.go` (`ReconcileNode`, `SetRemoteTraffic`, GUID merge,\n`synthNodeGuid`, `panelGuid`) and `service/node.go` (`effectiveNodeGuid`, heartbeat, dirty\ntracking). Node \"dirty\" flags drive an **anti-entropy reconciliation** so an offline node's\ninbound edits converge once it reconnects.\n\n**Where to look for node bugs:**\n\n- Operation not reaching a node → `runtime/remote.go` + `runtime/manager.go`.\n- Wrong traffic/online attribution across hops → `service/inbound_node.go` (GUID merge paths).\n- Node shown offline / stale status → `job/node_heartbeat_job.go` + `service/node.go` (`Probe`, `UpdateHeartbeat`).\n- Edits to an offline node not applying on reconnect → dirty/reconcile logic in `service/inbound_node.go` + `service/node.go` (`MarkNodeDirty`/`ClearNodeDirty`/`NodeSyncState`).\n- TLS/mTLS handshake failures → `runtime/tls_client.go`, `service/node_mtls.go`, `service/node.go` (`FetchCertFingerprint`).\n\n### 5.3 Traffic accounting\n\nPer-client and per-inbound up/down counters originate from Xray's stats API and are persisted\nto the DB. The Xray traffic job polls the core; node traffic is pulled from child nodes and\nmerged with GUID-based baselines to avoid double counting after resets.\n\n**Key files:** `service/inbound_traffic.go`, `service/traffic_writer.go`,\n`job/xray_traffic_job.go`, `job/node_traffic_sync_job.go`, `service/inbound_node.go`\n(`SetRemoteTraffic` / `upsertNodeBaseline`), models `xray.ClientTraffic`,\n`model.NodeClientTraffic`, `model.ClientGlobalTraffic` (cross-master totals).\nA client reset is queued per hosting node in `model.NodePendingReset` (`service/node_reset_queue.go`)\nand replayed by the node sync until the node accepts it.\nPeriodic resets: `job/periodic_traffic_reset_job.go` (keyed off `Inbound.TrafficReset`).\n\n### 5.4 Background jobs (cron)\n\nAll registered in `web.go` → `startTask()`. Each is a struct with a `Run()` method in `internal/web/job/`:\n\n| Schedule            | Job                                                                                              | Purpose / condition                                                                   |\n| ------------------- | ------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------- |\n| `@every 1s`         | `check_xray_running_job`                                                                         | Restart Xray if it died (2 consecutive down checks)                                   |\n| `@every 30s`        | (inline func in `startTask`)                                                                     | Debounced Xray restart — consumes the \"need restart\" flag (§5.1)                      |\n| `@every 5s`         | `xray_traffic_job`                                                                               | Pull traffic stats from Xray (5s start delay)                                         |\n| `@every 5s`         | `node_heartbeat_job`                                                                             | Probe child nodes (online/offline)                                                    |\n| `@every 5s`         | `node_traffic_sync_job`                                                                          | Pull + merge node traffic; push reconciliation                                        |\n| `@every 10s`        | `check_client_ip_job`                                                                            | Enforce per-client IP limits                                                          |\n| `@every 10s`        | `mtproto_job`                                                                                    | Reconcile `mtg` sidecars against enabled MTProto inbounds                             |\n| `@every 10s`        | `amneziawg_job`                                                                                  | Reconcile embedded AmneziaWG interfaces against enabled local inbounds                |\n| `@every 5m`         | `outbound_subscription_job`                                                                      | Refresh outbound provider configs                                                     |\n| `@every 10m`        | `clear_logs_job` (`PruneXrayLogsJob`)                                                            | Truncate Xray access/error logs once either exceeds 64 MiB                            |\n| `@hourly`           | `warp_ip_job`, `periodic_traffic_reset_job(\"hourly\")`                                            | WARP IP rotation; traffic resets                                                      |\n| `@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       |\n| `@weekly`           | `periodic_traffic_reset_job(\"weekly\")`                                                           | Weekly traffic resets                                                                 |\n| default `@every 1m` | `ldap_sync_job`                                                                                  | Only if LDAP enabled; schedule configurable                                           |\n| default `@daily`    | `stats_notify_job`                                                                               | Only if TG bot enabled; schedule configurable                                         |\n| default `@daily`    | `discord_notify_job`                                                                             | Only if Discord bot enabled; schedule configurable                                    |\n| `@every 2m`         | `check_hash_storage`                                                                             | Only if TG bot enabled; expires bot callback hashes                                   |\n| `@every 1m`         | `check_cpu_usage`                                                                                | Only if a CPU alarm is configured (TG, Discord, or email); publishes `cpu.high`       |\n| `@every 1m`         | `check_memory_usage`                                                                             | Only if a memory alarm is configured (TG, Discord, or email); publishes `memory.high` |\n| configurable        | `free_os_memory`                                                                                 | Only if `sys.MemoryReleaseIntervalMinutes() > 0`; returns heap to OS                  |\n\nTo change _when_ something runs, edit `startTask()`. To change _what_ it does, edit the job file.\n\n### 5.5 Type generation (Go → TypeScript) ⚠️ don't hand-edit generated files\n\nThe Go backend is the schema source of truth. `tools/openapigen` (a Go program, with a\n`StructAllow` allowlist of exported types) emits\n`frontend/src/generated/{schemas,types,zod,examples}.ts`. The frontend build runs this first:\n\n- `npm run gen:zod` → `go run ./tools/openapigen` (regenerate from Go)\n- `npm run gen:api` → builds the OpenAPI doc (`scripts/build-openapi.mjs`, driven by the\n  hand-maintained endpoint registry `src/pages/api-docs/endpoints.ts`)\n- `npm run build` runs `gen:api` then `vite build`.\n\n**Implication:** if you change a Go model/DTO that crosses the API boundary, regenerate the\nfrontend types (`cd frontend && npm run gen`) instead of editing `src/generated/` by hand.\n\n### 5.6 Share-link / subscription generation\n\nTwo distinct code paths produce client configs:\n\n- **Per-client links in the panel** (the \"copy link\" / QR in the UI): `service/client_link.go`\n  - `util/link/outbound.go`.\n- **Subscription endpoint** (what a client app polls): `internal/sub/service.go` (raw links),\n  `internal/sub/json_service.go` (JSON), `internal/sub/clash_service.go` (Clash YAML).\n  **`Host` rows** (`model.Host`, edited under /panel/api/hosts) override address/SNI/path/\n  security per inbound in subscription output — applied in `sub/host_sub.go`.\n\nBoth paths must agree per protocol. A malformed link for a specific protocol/transport combo\n(e.g. XHTTP + Reality) is usually a field-lookup mismatch in **`internal/sub/service.go`** (and\nits tests `service_test.go` / golden fixtures), or in `util/link/outbound.go`. The frontend\nalso has protocol schemas under `frontend/src/schemas/protocols/` and `frontend/src/lib/xray/`.\n\n### 5.7 Event bus (in-process pub/sub)\n\n`internal/eventbus/` is a minimal buffered-channel pub/sub. Producers call a non-blocking\n`Publish(Event)`; all subscribers receive every event. Event types: `outbound.down|up`,\n`xray.crash`, `node.down|up`, `cpu.high`, `memory.high`, `login.attempt`, with structured\npayloads (OutboundHealthData, NodeHealthData, LoginEventData, SystemMetricData). Producers\ninclude the CPU/memory jobs, node heartbeat, and login handling; consumers include the\nTelegram bot, the Discord bot (`service/discord/`), and the email notifier (`service/email/`). Use it for cross-cutting\nnotifications instead of importing notification services into producers.\n\n### 5.8 Tunnel health monitor\n\n`internal/tunnelmonitor/` is an optional watchdog configured **only via env vars**\n(`XUI_TUNNEL_HEALTH_*`, read in `internal/config/`), deliberately independent of panel\nsettings so it can be enabled from a systemd `EnvironmentFile` even when the panel is\nunreachable. It periodically probes an HTTP URL (default: Cloudflare trace endpoint) through\nthe tunnel; after N successive failures (default 3) it fires a recovery callback wired to an\nXray restart.\n\n---\n\n## 6. Data model cheat-sheet\n\nGORM models in `internal/database/model/` (main file `model.go` + siblings); all registered\nfor AutoMigrate in `internal/database/db.go`.\n\n| Model                           | Table role                                | Notable fields                                                                                                                                                     |\n| ------------------------------- | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| `User`                          | Admin login                               | bcrypt password, `LoginEpoch` (invalidates sessions)                                                                                                               |\n| `Inbound`                       | An Xray inbound                           | `Tag` (unique), `Port`, `Protocol`, `Settings`/`StreamSettings`/`Sniffing` (JSON), `Enable`, `TrafficReset`, `NodeID`, **`OriginNodeGuid`**, `ClientStats` (assoc) |\n| `Client`                        | In-memory client view                     | UUID/email/flow/limits (parsed from inbound JSON; not persisted)                                                                                                   |\n| `ClientRecord`                  | Persisted client (`clients`)              | `Email` (unique), `SubID`, `UUID`, `TotalGB`, `ExpiryTime`, `LimitIP`, `Group`, `Reset`                                                                            |\n| `ClientGroup` / `ClientInbound` | Grouping + client↔inbound join            | many-to-many wiring, `FlowOverride`                                                                                                                                |\n| `ClientExternalLink`            | Extra links attached to a client          | `Kind`, `Value`, `Remark`, `SortIndex`                                                                                                                             |\n| `Host`                          | Subscription host overrides (per inbound) | `Address`, `Port`, `Sni`, `Path`, `Security`, `Fingerprint`, `SortOrder`, visibility/exclusion flags                                                               |\n| `Node`                          | A managed child panel                     | `Guid`, `Address`, `Status`, `TlsVerifyMode`, `PinnedCertSha256`, `ConfigDirty`, version/heartbeat/metric fields                                                   |\n| `NodeClientTraffic`             | Per-node client traffic baseline          | cross-node merge (anti-double-count)                                                                                                                               |\n| `NodePendingReset`              | Client resets a node has not confirmed    | `NodeId`, `Email`, `QueuedAt`; replayed by the node sync, freezes that client's node verdict until delivered                                                       |\n| `NodeClientIp`                  | Per-node client IP attribution            | `NodeGuid`, `Email`, `Ips`                                                                                                                                         |\n| `ClientGlobalTraffic`           | Cross-master usage totals                 | `MasterGuid`, `Email`, `Up`, `Down`                                                                                                                                |\n| `xray.ClientTraffic`            | Per-client counters (`client_traffics`)   | `Email`, `Up`, `Down`, `Total`, `ExpiryTime`, `LastOnline`                                                                                                         |\n| `InboundClientIps`              | IP set per client email                   | drives IP-limit enforcement                                                                                                                                        |\n| `OutboundTraffics`              | Outbound counters                         | per outbound tag                                                                                                                                                   |\n| `OutboundSubscription`          | External provider subs                    | Warp/Nord style                                                                                                                                                    |\n| `Setting`                       | Key/value panel settings                  | everything configurable                                                                                                                                            |\n| `ApiToken`                      | REST API tokens                           | SHA-256 hash (plaintext shown once)                                                                                                                                |\n| `InboundFallback`               | Fallback routing on a shared port         | SNI/ALPN/path → dest                                                                                                                                               |\n| `HistoryOfSeeders`              | Seeder bookkeeping                        | prevents re-running one-off migrations                                                                                                                             |\n\n---\n\n## 7. Symptom → File index (start here when debugging)\n\n| Symptom / task                                                                    | Primary file(s)                                                              | Then check                                                                                          |\n| --------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- |\n| 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`                            |\n| **Inbound** create/update/delete behavior                                         | `service/inbound.go`, `service/inbound_clients.go`                           | `runtime/*`, `service/xray.go`                                                                      |\n| **Client** CRUD / limits / expiry                                                 | `service/client_crud.go`, `service/client_inbound_apply.go`                  | model `ClientRecord`, `service/inbound_traffic.go`                                                  |\n| **Bulk** client operations slow/wrong                                             | `service/client_bulk.go`                                                     | `service/client_paging.go`                                                                          |\n| Xray **won't apply** a config change                                              | `service/xray.go` (`RestartXray`, `tryHotApply`)                             | `xray/hot_diff.go`, `xray/process.go`                                                               |\n| Xray **restarts when it shouldn't** (kills connections)                           | `xray/hot_diff.go` (diff not classified as hot)                              | `service/xray.go`                                                                                   |\n| **Traffic** counts wrong / reset behavior                                         | `service/inbound_traffic.go`, `job/xray_traffic_job.go`                      | `service/traffic_writer.go`, `job/periodic_traffic_reset_job.go`                                    |\n| **Node** operation not propagating                                                | `runtime/remote.go`, `runtime/manager.go`                                    | `service/inbound_node.go`                                                                           |\n| **Multi-hop / cross-node attribution** (traffic or online clients on wrong panel) | `service/inbound_node.go` (GUID merge, `synthNodeGuid`, `effectiveNodeGuid`) | `service/node.go`, model `OriginNodeGuid`/`Node.Guid`                                               |\n| Node stuck **offline / stale**                                                    | `job/node_heartbeat_job.go`, `service/node.go` (`Probe`, `UpdateHeartbeat`)  | `runtime/tls_client.go` (TLS verify)                                                                |\n| Node **TLS / mTLS** auth failures                                                 | `runtime/tls_client.go`, `service/node_mtls.go`, `service/setting_mtls.go`   | `service/node.go` (`FetchCertFingerprint`)                                                          |\n| Offline node edits **not reconciling** on reconnect                               | `service/inbound_node.go` (`ReconcileNode`, dirty flags)                     | `service/node.go` (`MarkNodeDirty`/`NodeSyncState`)                                                 |\n| **Share link / QR** malformed (per protocol)                                      | `service/client_link.go`, `util/link/outbound.go`                            | `frontend/src/lib/xray/`, `frontend/src/schemas/protocols/`                                         |\n| **Subscription** output wrong (raw/JSON/Clash)                                    | `internal/sub/service.go`                                                    | `sub/json_service.go`, `sub/clash_service.go`, sub golden tests                                     |\n| Subscription **host overrides** not applied                                       | `service/host.go`, `sub/host_sub.go`                                         | model `Host`, `frontend/src/pages/hosts/`                                                           |\n| **External subscription** import/aggregation                                      | `sub/external_subscription.go`, `sub/external_config.go`                     | `sub/clash_external.go`                                                                             |\n| **Settings** not saving / defaults                                                | `service/setting.go`, `controller/setting.go`                                | model `Setting`                                                                                     |\n| **Login / 2FA / sessions / CSRF**                                                 | `controller/index.go`, `service/panel/user.go`, `middleware/`                | `session/`                                                                                          |\n| **API tokens**                                                                    | `service/panel/api_token.go`, `controller/setting.go`                        | model `ApiToken`                                                                                    |\n| **Port conflict** on inbound add                                                  | `service/port_conflict.go`                                                   | `controller/inbound.go`                                                                             |\n| **Fallbacks** (shared 443, SNI routing)                                           | `service/fallback.go`, `controller/inbound.go`                               | model `InboundFallback`                                                                             |\n| **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()` |\n| **`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/`                            |\n| **Telegram bot** commands                                                         | `service/tgbot/`                                                             | `job/stats_notify_job.go`                                                                           |\n| **Discord bot** commands & reports                                                | `service/discord/`                                                           | `job/discord_notify_job.go`                                                                         |\n| **Email notifications**                                                           | `service/email/`                                                             | `internal/eventbus/` (consumers)                                                                    |\n| **CPU / memory alerts** not firing                                                | `job/check_cpu_usage.go`, `job/check_memory_usage.go`                        | `internal/eventbus/`, notifier settings in `service/setting.go`                                     |\n| Xray auto-restart on **dead tunnel**                                              | `internal/tunnelmonitor/`                                                    | `XUI_TUNNEL_HEALTH_*` in `internal/config/`                                                         |\n| **WARP / Nord / PIA** outbound integration                                        | `service/integration/warp.go` / `nord.go` / `pia.go`                         | `internal/pia/`, `frontend/src/pages/xray/overrides/`                                               |\n| **MTProto** proxy issues                                                          | `internal/mtproto/manager.go`, `mtproto/process*.go`                         | `job/mtproto_job.go`                                                                                |\n| **DB migration** / new column                                                     | `internal/database/db.go` (AutoMigrate list), `migrate_data.go`              | `model/model.go`                                                                                    |\n| **Cron schedule** changes                                                         | `web.go` → `startTask()`                                                     | the specific `job/*.go`                                                                             |\n| **CORS / security headers / HTTPS**                                               | `middleware/`, `web.go` (`initRouter`, TLS setup)                            | `config/` (env)                                                                                     |\n| **Env vars / paths / DB type**                                                    | `internal/config/config.go`                                                  | `.env.example`                                                                                      |\n| **Frontend route / screen**                                                       | `frontend/src/pages/<area>/`, `frontend/src/routes.tsx`                      | `frontend/src/api/queries/`                                                                         |\n| **Frontend ↔ backend type mismatch**                                              | regenerate: `cd frontend && npm run gen` (`tools/openapigen`)                | `frontend/src/generated/`                                                                           |\n| **System status / CPU / metrics**                                                 | `service/server.go`, `service/xray_metrics.go`, `service/metric_history.go`  | `controller/server.go`, gopsutil                                                                    |\n\n---\n\n## 8. Layering rules (where new code belongs)\n\n1. **Controllers are thin.** Only: bind/validate input, call one service, shape the HTTP\n   response. No DB queries, no Xray calls, no business rules in `controller/`.\n2. **Services own the logic and transactions.** All business rules, DB access, and decisions\n   about applying changes live in `service/`. If you're tempted to query GORM from a\n   controller, move it to a service.\n3. **Never touch Xray's running state from a controller or job directly.** Go through\n   `XrayService` / the `runtime.Runtime` interface so local vs node dispatch stays correct.\n4. **Any state-changing inbound/client o"},"files":{"frontend/CLAUDE.md":"# frontend/CLAUDE.md\n\nFrontend agent guide. Full detail: `frontend/README.md` and the root\n`CONTRIBUTING.md` (\"Working on the frontend\"). This is the short version.\n\n## What this is\nReact 19 + Ant Design 6 + Vite 8 + TypeScript. The Vite config is\n`vite.config.js` (plain JS). Three bundles, each emitted into\n`internal/web/dist/` and embedded into the Go binary:\n- `index.html` — admin panel SPA (entry `src/main.tsx`; react-router under\n  `/panel`, lazy routes).\n- `login.html` — login + 2FA (`src/entries/login.tsx`).\n- `subpage.html` — public subscription viewer (`src/entries/subpage.tsx`).\nThe `@` import alias maps to `src/`.\n\n## Data flow\n- Server state via TanStack Query (`src/api/`, keys in `src/api/queryKeys.ts`);\n  invalidate on mutation. WebSocket pushes feed the cache\n  (`src/api/websocketBridge.ts`).\n- Local UI state in the page (`useState`); shared concerns via `src/hooks/`.\n  Extend an existing hook before adding a global.\n- Zod (`src/schemas/`) is the single source of truth for the xray config model.\n  Infer types with `z.infer`. Go-side types are mirrored into `src/generated/`\n  by `npm run gen:zod` (`go run ./tools/openapigen`) — do not hand-edit that\n  folder (every file is marked `DO NOT EDIT`).\n- xray domain logic (links, defaults, form<->wire adapters) is pure functions in\n  `src/lib/xray/`. HTTP goes through `HttpUtil` in `src/utils/index.ts`.\n\n## Rules\n- Ant Design 6 for components; no Tailwind/shadcn (a migration was rolled back).\n  Form *state* runs on React Hook Form (`src/components/form/rhf/`), not Ant\n  Design's `Form` store.\n- Function components + hooks only; no class components.\n- Comments in committed TS/TSX: 2 lines MAX per comment block, spent on the\n  *why* a name cannot hold (same rule as root CLAUDE.md). HTML comments are fine.\n- TS strict; oxlint's `typescript/no-explicit-any` is an error. Build forms with `useZodForm` +\n  `FormField` from `@/components/form/rhf` (wrap the tree in `FormProvider`);\n  validate through the `zodResolver` or per-field\n  `rules={{ validate: rhfZodValidate(Schema.shape.field) }}` — messages are Zod\n  issue keys resolved via `t()`, never inline `z.string()`. AntD `<Form>` stays\n  only as a layout wrapper. Complex shared config editors (FinalMask / Sniffing /\n  Sockopt) remain AntD-`Form` islands wrapped as value/onChange adapters in\n  `src/lib/xray/forms/fields/`, bound via a `Controller`.\n- New `g.POST`/`g.GET` route => add it to `src/pages/api-docs/endpoints.ts`,\n  then `npm run gen`.\n- i18n strings live in `internal/web/translation/<locale>.json`, NOT under\n  `frontend/`, and are shared with the Go backend. A new English key must be\n  added to every locale. Interpolation here uses single braces `{var}`, not the\n  i18next default `{{var}}`.\n- Persian/Arabic (RTL) users are first-class — isolate code identifiers on their\n  own line when writing Persian text in labels/toasts.\n- Vite is pinned to an exact version (no `^`) — bump deliberately, then verify\n  `npm run dev` AND `npm run build`.\n\n## Adding a panel route\n1. `src/pages/<page>/<Page>.tsx` (kebab folder, PascalCase component).\n2. Register in `src/routes.tsx` under `/panel` (lazy import).\n3. Add a sidebar link in `src/layouts/AppSidebar.tsx` if it needs nav.\nOnly standalone bundles (login/subpage) need a new `.html` + `src/entries/*` +\n`rollupOptions.input` (in `vite.config.js`) + a Go controller route.\n\n## Commands\n- `npm run dev` (HMR on :5173, proxies to the Go panel on :2053 — start Go first).\n- `npm run typecheck` / `npm run lint` / `npm run test` / `npm run build`.\n- `npm run gen` = `gen:zod` (Go → `src/generated/`) + `gen:api`\n  (`build-openapi.mjs` → `public/openapi.json`).\n- `npm run storybook` (workbench on :6006) / `npm run build-storybook` (CI\n  compile-checks every story). Reusable `src/components/` get a co-located\n  `<Component>.stories.tsx` with `tags: ['autodocs']`; document props via\n  `argTypes` / `parameters.docs` string metadata, never JSDoc.\n- After `npm run build`, RESTART `go run .` (see the XUI_DEBUG gotcha in root\n  CLAUDE.md) before checking the panel.\n","CLAUDE.md":"# CLAUDE.md\n\nOperational guide for AI agents working in this repo. Long-form human docs:\n`CONTRIBUTING.md` (setup, testing philosophy) and `frontend/README.md`.\nRead those before large changes. This file is the short, must-follow version.\nFor a deep navigation map (request lifecycle, cron-job table, symptom → file\nindex, layering rules), read `docs/architecture.md` on demand — do not guess\nfile locations when it can answer in one hop.\n\n## Stack\n- Backend: Go 1.27 (`module github.com/mhsanaei/3x-ui/v3`), Gin, GORM.\n  Runs Xray-core as a managed child process (`internal/xray/process.go`) and\n  imports `github.com/xtls/xray-core` for config types + gRPC stats/handler/router\n  API. MTProto inbounds run a second managed child — the `mtg-multi` binary\n  (a multi-secret mtg fork — NOT a Go dependency; its prebuilt release binary is\n  fetched at image/release build time by `DockerInit.sh` + `release.yml`,\n  panel-side code in `internal/mtproto/`) — outside Xray, one process per inbound\n  serving each\n  client's FakeTLS secret via the fork's `[secrets]` section (plus per-client\n  ad-tags via `[secret-ad-tags]` and per-client data quota / expiry via\n  `[secret-limits]`, mapped from the client's `totalGB`/`expiryTime`). Client,\n  ad-tag and quota/expiry edits are hot-applied through the fork's management API\n  (`PUT /secrets`, bearer-token guarded) so connections survive; the manager\n  falls back to a process restart on older binaries. A client's panel-side\n  traffic reset also calls `POST /secrets/{name}/reset-quota` so a renewed client\n  is not re-blocked by the sidecar's quota counter.\n- Storage: SQLite by default (`/etc/x-ui/x-ui.db` on Linux; the executable dir on\n  Windows), PostgreSQL optional (`XUI_DB_TYPE` / `XUI_DB_DSN`). The CGo SQLite\n  driver (`mattn/go-sqlite3`) needs a C compiler — `CGO_ENABLED=0` builds fail.\n- Frontend: React 19 + Ant Design 6 + Vite 8 + TypeScript in `frontend/`,\n  built into `internal/web/dist/` (gitignored) and embedded via `embed.FS`.\n\n## Repo map\n- `main.go` — entry point + `x-ui` CLI (run, migrate, migrate-db, encrypt-tokens, setting, cert).\n- `internal/config/` — env parsing (XUI_DEBUG, XUI_LOG_LEVEL, XUI_LOG_FOLDER,\n  XUI_BIN_FOLDER, XUI_SKIP_HSTS, XUI_PORT, XUI_DB_*).\n- `internal/database/` + `internal/database/model/` — GORM schema (~24 models;\n  Inbound, Client, Setting, User are the core), inbound Protocol enum,\n  AutoMigrate + hand-written migrations in `db.go`.\n- `internal/xray/` — Xray child-process lifecycle, config generation, gRPC API.\n- `internal/xray/geodata/` — streaming geosite/geoip `.dat` reader (cached\n  category index + paged entries) and `geosite:`/`geoip:`/`ext:` token parsing.\n- `internal/mtproto/` — MTProto inbounds via the bundled `mtg-multi` binary.\n- `internal/tuic/` — TUIC v5 inbounds: `tuic-server` sidecar supervisor, native Go UDP relay traffic metering.\n- `internal/amneziawg/` — AmneziaWG protocol shape: instance/peer derivation\n  from an inbound, 3.1 obfuscation param generation + validation, port-forward\n  spec parsing.\n- `internal/amneziawgnet/` — embedded AmneziaWG runtime: amneziawg-go device\n  over a gVisor userspace netstack, per-inbound reconcile manager, TCP/UDP\n  relay into a loopback per-peer-auth SOCKS5 Xray inbound, port-forward\n  listeners, per-peer IPv6 egress aliases.\n- `internal/pia/` — PIA WireGuard protocol client (auth, signed server list, `/addKey`).\n- `internal/sub/` — subscription server (raw / JSON / Clash).\n- `internal/eventbus/` — in-process pub/sub (outbound/node health, xray.crash,\n  cpu.high, memory.high, login.attempt).\n- `internal/logger/`, `internal/util/` (link, crypto, sys, ldap, …),\n  `internal/tunnelmonitor/` — shared infrastructure.\n- `internal/web/` — Gin server (embeds `dist/` + `translation/`).\n  - `controller/` — panel + REST API handlers; OpenAPI at /panel/api/openapi.json.\n  - `service/` — business logic (InboundService, SettingService, XrayService,\n    node sync); subpackages tgbot/, discord/, email/, outbound/, panel/, integration/.\n  - `job/` — 19 cron jobs (traffic, fail2ban IP-limit, node heartbeat/sync, LDAP,\n    CPU/memory watchdogs, …); full table in `docs/architecture.md` §5.4.\n  - `middleware/`, `entity/`, `global/`, `session/` (CSRF), `network/`,\n    `runtime/` (master/sub-node over mTLS), `websocket/`.\n  - `locale/` + `translation/` — i18n, 13 embedded locale JSON files.\n- `frontend/` — React + TS source (see `frontend/CLAUDE.md`).\n- `tools/openapigen/` — Go generator that emits frontend types + Zod/JSON schemas\n  into `frontend/src/generated/` from Go structs. The OpenAPI doc itself\n  (`frontend/public/openapi.json`) is assembled from those + `endpoints.ts` by\n  `frontend/scripts/build-openapi.mjs`.\n- `docs/` — separate Next.js/Fumadocs site (pnpm, own CI in `docs-ci.yml`,\n  outside `make verify`). Holds a THIRD independent implementation of\n  link/subscription generation in `docs/lib/xray/` — check it whenever\n  share-link or install-command output changes.\n\n## Hard rules (non-negotiable)\n- Correct fix over small fix. Find the root cause and fix it the right way, however\n  much code that takes. Size the change by what the correct fix needs, never by\n  line count: when the right fix spans many files, or needs a migration, a shared\n  helper or a new abstraction, write it. A guard that hides the symptom while the\n  cause survives is the wrong fix, however small. Two limits remain:\n  - Everything added must be something the correct fix needs. No speculative\n    knobs, unused extension points or \"while I was here\" rewrites. Unrelated\n    refactors and cleanups go in their own commit.\n  - Stop and ask only when the right fix needs a decision the code cannot answer:\n    a deliberate user-visible behaviour change, or two sound designs with a real\n    trade-off. Ask with a recommendation. Size alone is never a reason to stop,\n    defer or ship a smaller patch.\n- Comments in committed Go/TS: 2 lines MAX per comment block. Make the name\n  carry the meaning first and rename rather than annotate; spend the 2 lines on\n  the *why* a name cannot hold — an invariant, an issue number, a non-obvious\n  constraint. Exempt: `//go:build`, `//go:generate`, and other directives.\n  HTML `<!-- -->` is fine. (A linter cannot enforce this — you must.)\n- New `g.POST`/`g.GET` in `internal/web/controller/` REQUIRES a matching entry\n  in `frontend/src/pages/api-docs/endpoints.ts`, then `make gen` (or\n  `cd frontend && npm run gen`). Hand-maintained but pinned both ways by\n  `TestRouteRegistryContract` (`internal/web/routes_contract_test.go`): a missing\n  OR stale entry fails `make test-go`. Scope: `/panel/api/*` + a few session\n  routes; sub-server routes are exempt.\n- Response examples come from Go struct `example:` tags via `tools/openapigen` —\n  never hand-write them. A new struct must be added to openapigen's `StructAllow`\n  allowlist (`tools/openapigen/main.go`) or it is silently omitted from\n  schemas/examples (and `build-openapi.mjs` then fails on the missing schema).\n- A new or renamed endpoint has a FOURTH step nothing checks: copy\n  `frontend/public/openapi.json` → `docs/public/openapi.json`, then\n  `cd docs && pnpm gen:api` to refresh the MDX under\n  `docs/content/docs/en/reference/api/`. `docs-ci.yml` fires only on `docs/**`.\n- A new English i18n key goes in EVERY locale JSON in `internal/web/translation/`\n  (13 files) AND must be referenced from `frontend/src` or Go in the SAME commit —\n  `frontend/src/test/i18n-dead-keys.test.ts` fails both ways. It is a frontend\n  test, so run `npm test`, not just `make test-go`. At runtime the frontend falls\n  back to en-US; Go (`internal/web/locale/`) returns \"\" for an unknown key.\n- DB / model changes require a migration in `internal/database/db.go`.\n- Every state-changing inbound/client op dispatches through `runtime.Runtime`\n  (`internal/web/runtime/`) — never straight to `internal/xray/api.go`, never from\n  a controller or cron job. A direct call passes every local test and silently\n  breaks every multi-node deployment. Other layering rules: `docs/architecture.md` §8.\n- Conventional commits: `type(area): short imperative summary`, then a body\n  explaining the why. Types in use: `fix`, `feat`, `chore`, `refactor`, `perf`,\n  `docs`, `style`.\n\n## Tests: TDD, and only tests that can fail (Go and frontend)\n- Work red → green → refactor.\n  - Bug: turn the reproduction into a test first, and watch it fail for the\n    reported reason.\n  - Feature: write the test for the first behaviour before writing its code.\n  - Then write the code that makes it pass, and refactor with the suite green.\n\n  If a test was written after the code, prove it anyway: revert the code, watch\n  the test go red, then restore. A test that passes either way is worse than no\n  test. It certifies nothing, and then gets cited as proof the fix works.\n- Every test must name the failure it catches. When no test can reach a change\n  (workflow YAML, pure wiring, layout), say so and name the command that\n  demonstrates it. Never write a stand-in test.\n- Fake tests are forbidden. Delete any you write or meet in the code you touch:\n  - tests of a getter, a constant, a rename, a pure map lookup, or an input the\n    function can never receive;\n  - tests that restate the implementation, such as recomputing the expected\n    value with the same formula or asserting that a mock was called exactly the\n    way the code calls it;\n  - mocking the unit under test, or mocking so much around it that the real\n    code path never runs;\n  - assertions too weak to fail: `err != nil`, `len > 0`, `toBeDefined()`, or\n    `not.toThrow()` alone;\n  - golden files or snapshots regenerated to match whatever the code now outputs;\n  - extra cases that exercise no distinct branch, and tests written to raise\n    coverage.\n\n  One real test that drives the bug through the actual code path beats five\n  that restate the code.\n\n## Go conventions\n- Stdlib `testing` only (no testify). Table-driven, `t.Run` subtests,\n  `t.Helper()` on helpers. Assert the exact value / typed error / emitted\n  string, never just `err != nil`. Prefer real deps over mocks: throwaway DB via\n  `dbtest.InitDB(t, filepath.Join(t.TempDir(), \"x-ui.db\"))`\n  (`internal/database/dbtest`: copies a once-migrated template and registers\n  `CloseDB` cleanup; a fresh `database.InitDB` costs ~7x more, ~850ms under\n  `-race`); `httptest` for HTTP. Keep `database.InitDB` for reopening a file or\n  migrating a hand-built legacy DB. `internal/sub`'s `initSubDB(t)` is the template.\n- Code must pass `golangci-lint run` (gofumpt + goimports formatting): `make lint`.\n- Postgres, xray-gRPC-e2e, master+node and scale tests `t.Skip` unless\n  `XUI_TEST_PG_DSN`, `XUI_DB_TYPE`+`XUI_DB_DSN`, `XRAY_E2E_BINARY`,\n  `XUI_NODE_E2E_BINARY` or `XUI_SCALE_TEST` is set — a green `go test ./...`\n  does not mean those paths ran.\n\n## Frontend conventions (summary; full version in frontend/CLAUDE.md)\n- Ant Design 6 only — no Tailwind/shadcn. Targeted tweaks, not rewrites.\n- TS strict; `@typescript-eslint/no-explicit-any` is an error. Zod schemas in\n  `src/schemas/` are the source of truth; infer types with `z.infer`, never\n  hand-write. Do not edit `src/generated/`.\n- Node 26 (`.nvmrc`) — `make gen` imports `.ts` directly and needs its type\n  stripping; Node 22 dies with `ERR_UNKNOWN_FILE_EXTENSION`. `npm test` includes\n  a headless-Chromium Storybook project, so run\n  `npx playwright install --with-deps chromium` once or `make verify` fails.\n- Editing `frontend/src` does NOT change what users see until the Vite build is\n  regenerated into `internal/web/dist/`. In `XUI_DEBUG=true`, HTML is served from\n  the frozen embedded FS but JS/CSS off disk — after `npm run build` you MUST\n  restart `go run .` or you get a blank page with 404s.\n- After touching share-link logic (`src/lib/xray/`), run `npm run test` (golden\n  fixtures); regenerate snapshots (`npx vitest run -u`) only for intentional\n  output changes, never to make a red test green.\n\n## Build, test, verify\nA fresh clone has no `internal/web/dist/`, so a bare `go build ./...` dies with\n`pattern all:dist: no matching files found` while ~35 other packages pass — it\nreads as a broken repo, not a missing step. Run `make dist-stub` once; every\n`make` Go target already depends on it, which is why `make test-go` beats\n`go test ./...`. Run `make help` for all targets. The local gate:\n\n    make verify   # gen-check + lint + typecheck + test + build + build-storybook\n\nThat is the *fast* gate, not all of CI. `ci.yml` also runs `make race`,\n`make vulncheck`, a live-Postgres job (where a SKIP counts as a failure),\n`make node-e2e` (a real master and node panel, `internal/nodee2e/`) and a\n30s fuzz smoke on `FuzzParseLink`/`FuzzDecodeCertPin` — run those locally when\nyou touch DB/dialect, node sync or parser code. Node sync has two layers: every\n`runtime.Remote` call gets a cell in `internal/web/node_contract_test.go` (fast,\nin `make test-go`; a method without one fails it), and a multi-tick flow (cron,\nadopt, node down) gets one in `internal/nodee2e/node_sync_test.go`.\n\nCommon targets: `make gen` (regenerate Zod/OpenAPI), `make lint` (Go + frontend),\n`make test` (Go `-shuffle=on` + frontend), `make race`, `make build`. See `Makefile`.\n\n## Definition of done (before opening a PR)\n1. `make verify` passes — its `gen-check` already runs `make gen` and fails on a\n   dirty `frontend/src/generated` / `frontend/public/openapi.json`.\n2. Diff is focused; refactors are separate from feature work.\n","docs/architecture.md":"# 3x-ui — Architecture & Code Map\n\n> Navigation map for contributors and AI coding agents (referenced from `CLAUDE.md`).\n> Goal: jump to the right file in one hop instead of grepping the whole tree.\n> Tracks the `main` branch — paths reflect the latest changes, so verify against the live\n> tree rather than a pinned release (Go module `github.com/mhsanaei/3x-ui/v3`).\n>\n> **How to use this file:** read \"Mental model\" + \"Request lifecycle\" first, then\n> use the **Symptom → File index** to locate work. Respect the **Layering rules**\n> when adding code. Verify with the commands in **Build / Test / Lint**.\n\n---\n\n## 1. Mental model (the 30-second version)\n\n3x-ui is a **web control panel for [Xray-core](https://github.com/XTLS/Xray-core)**. The Go\nbackend is the source of truth: it stores inbounds/clients/settings in a DB, renders an\nXray JSON config from that state, supervises the Xray child process, and exposes a REST +\nWebSocket API. A React SPA (built by Vite, embedded into the Go binary) is the UI. A second,\nseparate HTTP server serves **subscription links** to end users.\n\nThe panel supervises **managed child processes**: Xray-core itself and — when MTProto\ninbounds exist — a dedicated child proxy binary:\n\n- **`mtg-multi` for MTProto inbounds** (`github.com/mhsanaei/mtg-multi`, a multi-secret fork\n  built from source; `internal/mtproto/`): One process per inbound serves every attached\n  client's FakeTLS secret through the fork's `[secrets]` section, plus optional per-client\n  sponsored-channel ad-tags via `[secret-ad-tags]`. A client or ad-tag edit is hot-applied via\n  the fork's management API (`PUT /secrets`, guarded by a per-process bearer token), with a\n  process restart as the fallback on older binaries.\n\nIn contrast, **AmneziaWG** (`internal/amneziawgnet/`) and **TUIC v5** (`internal/tuic/`) run as\n**in-process native Go servers** without external child processes, bridging client traffic into\nXray-core via loopback SOCKS5 relays.\n\nServers and processes, all launched from `main.go`:\n\n| Server / process | Package                           | Purpose                                                            | Default port      |\n| ---------------- | --------------------------------- | ------------------------------------------------------------------ | ----------------- |\n| **Panel**        | `internal/web`                    | Admin REST/WS API + serves the embedded SPA                        | 2053              |\n| **Subscription** | `internal/sub`                    | Public endpoint that hands out client configs (raw / JSON / Clash) | `subPort` setting |\n| **Xray-core**    | supervised via `internal/xray`    | The actual proxy engine; a child process, not Go code              | `inbounds[].port` |\n| **mtg-multi**    | supervised via `internal/mtproto` | MTProto proxy child process for MTProto inbounds (multi-secret)    | per inbound       |\n\nTwo key ideas that explain most of the complexity:\n\n1. **The DB → Xray config pipeline.** Inbounds/clients live in the DB. On every change the\n   backend regenerates the Xray config and applies it — preferring a _hot diff_ (live gRPC\n   API mutation) over a full process restart. See §5.1.\n2. **The Runtime abstraction (multi-node).** A panel can manage remote \"nodes\" (other 3x-ui\n   instances). Every state-changing inbound/client operation is dispatched through a\n   `runtime.Runtime` interface that is either **`Local`** (this box's Xray gRPC API) or\n   **`Remote`** (HTTPS call to a child node, with `verify`/`skip`/`pin`/`mtls` TLS modes).\n   This is the single most important abstraction in the project. See §5.2.\n\n---\n\n## 2. Tech stack\n\n**Backend (Go 1.27):**\n\n- Web framework: **Gin** (`gin-gonic/gin`) + sessions (cookie store), gzip.\n- ORM: **GORM** with **SQLite** (default) or **PostgreSQL** (`XUI_DB_TYPE=postgres`).\n- Scheduler: **robfig/cron/v3** (seconds-precision) for all background jobs.\n- Xray: **xtls/xray-core** vendored as a library; the panel talks to the running core over\n  its **gRPC API** and also shells out to manage the process.\n- Bots: Telegram bot (**mymmrac/telego**), Discord bot (Discord REST API v10 + **gorilla/websocket** Gateway v10). i18n: **nicksnyder/go-i18n**.\n- Misc: gorilla/websocket, gopsutil (system stats), go-qrcode, gotp (2FA TOTP).\n\n**Frontend (`frontend/`):**\n\n- **React 19** + **Ant Design 6** + **Vite 8** + **TypeScript**.\n- Data layer: **TanStack Query** (`@tanstack/react-query`) over the native **Fetch API**; **Zod 4** schemas.\n- Router: **react-router 8**. Charts: **uPlot** (`frontend/src/components/viz/Sparkline.tsx`). Editor: **CodeMirror 6**.\n- **Build output goes to `internal/web/dist/`** (see `vite.config.js` → `outDir`) and is\n  embedded into the Go binary with `go:embed`. Three HTML entries: `index.html` (panel SPA),\n  `login.html`, `subpage.html`. The Go server serves the SPA; there is no separate frontend\n  deployment.\n\n**Important:** the legacy Go-template UI and `web/assets/` are **gone**. All HTML/JS comes\nfrom the embedded Vite `dist/`. Don't look for `.html` templates in `internal/web`.\n\n---\n\n## 3. Request lifecycle (follow the data)\n\n### 3.1 Admin API request (e.g. \"add a client\")\n\n```\nBrowser (React, fetch)\n  → POST {basePath}/panel/api/...\n    → Gin engine (internal/web/web.go: initRouter)\n      → middleware chain: SecurityHeaders → MaxBodyBytes (10 MiB; importDB exempt)\n                          → [DomainValidator, if webDomain set] → gzip → sessions(\"3x-ui\")\n                          → base-path/cache-control context → Localizer\n                          → API routes add: ConfigEnvelope (zstd + SHA-256) → CSRF\n        → Controller (internal/web/controller/*.go)   // HTTP concerns only: bind, validate, respond\n          → Service (internal/web/service/*.go)        // business logic + transactions\n            → GORM → DB (internal/database)            // persistence\n            → runtime.Runtime dispatch                 // apply to Xray (Local) or node (Remote)\n              → Local:  internal/xray (gRPC API or config regen + restart)\n              → Remote: internal/web/runtime/remote.go → HTTPS → child node's API\n```\n\nThe controller layer is thin. **Business logic lives in services.** When something is wrong\nwith _behavior_, the bug is almost always in a service file, not a controller.\n\n### 3.2 Subscription request (end-user fetching their config)\n\n```\nEnd user → GET {subPath}/{subId}   (separate server, internal/sub)\n  → internal/sub/controller.go (routes: raw / JSON / Clash variants, feature-flagged)\n    → internal/sub/service.go (~2.5k lines — the link/config builder)\n      → reads inbounds+clients+hosts from DB, renders per-protocol share links /\n        Clash YAML / JSON (Host rows can override address/SNI/path per inbound)\n```\n\n### 3.3 Background work (cron jobs)\n\nScheduled in `internal/web/web.go` → `startTask()`. Each job is a struct in\n`internal/web/job/`. Examples: poll Xray traffic every 5s, check client IP limits every 10s,\nnode heartbeat every 5s, periodic traffic resets (hourly/daily/weekly/monthly). See §5.4.\n\n---\n\n## 4. Directory map (what lives where)\n\n```\n3x-ui/\n├── main.go                     # Entry point: CLI (run / migrate / migrate-db / setting / cert),\n│                               #   bootstrap, signal handling, restart loop\n├── go.mod / go.sum             # Go deps (module path ends in /v3)\n│\n├── internal/                   # ALL backend Go code (private packages)\n│   ├── config/                 # Env-var config: paths, DB kind/DSN, log level, version\n│   │                           #   Every XUI_* env var is read here (config.go)\n│   ├── database/\n│   │   ├── db.go               # InitDB: connect, AutoMigrate, seeders (~1.4k lines). DB hotspot.\n│   │   ├── migrate_data.go     # Data migrations (seeders/normalizers beyond AutoMigrate)\n│   │   ├── dialect.go          # SQLite vs Postgres SQL differences\n│   │   ├── dump_sqlite.go      # DB export/backup\n│   │   └── model/              # **ALL GORM models** (model.go ~1.1k lines + siblings:\n│   │                           #   node_client_traffic.go, node_client_ip.go,\n│   │                           #   client_global_traffic.go). ⭐ Start here for data shape.\n│   ├── pia/                    # PIA WireGuard protocol client (auth, signed server list, /addKey)\n│   ├── eventbus/               # In-process pub/sub (buffered channel): outbound.down|up,\n│   │                           #   xray.crash, node.down|up, cpu.high, memory.high, login.attempt\n│   ├── tunnelmonitor/          # Optional tunnel health probe (XUI_TUNNEL_HEALTH_* env vars):\n│   │                           #   HTTP probe (default Cloudflare trace); repeated failures\n│   │                           #   trigger an Xray restart hook. Independent of panel settings.\n│   ├── xray/                   # Xray-core integration (the proxy engine wrapper)\n│   │   ├── process.go          # Spawn/supervise the Xray child process (~750 lines)\n│   │   ├── api.go              # gRPC client to a running Xray (add/remove user, stats) (~800 lines)\n│   │   ├── hot_diff.go         # ⭐ Compute minimal live changes to avoid full restart (~500 lines)\n│   │   ├── config.go           # Xray config object model\n│   │   ├── inbound.go          # Inbound JSON shaping\n│   │   ├── client_traffic.go   # ClientTraffic model (persisted as client_traffics)\n│   │   ├── traffic.go          # Traffic type helpers\n│   │   ├── log_writer.go       # Pipe Xray stdout/stderr into the panel logger\n│   │   └── geodata/            # Browse geosite/geoip .dat: streaming protowire reader,\n│   │                           #   cached category index, routing-token parsing (token.go)\n│   │\n│   ├── web/                    # The panel server\n│   │   ├── web.go              # ⭐ Server bootstrap: initRouter (all routes) + startTask (all cron jobs)\n│   │   ├── controller/         # HTTP handlers (thin). One file per resource:\n│   │   │   ├── inbound.go      #   /panel/api/inbounds\n│   │   │   ├── client.go       #   /panel/api/clients (CRUD + bulk + ips + onlines)\n│   │   │   ├── group.go        #   client-group endpoints\n│   │   │   ├── node.go         #   /panel/api/nodes   (multi-node management)\n│   │   │   ├── host.go         #   /panel/api/hosts   (per-inbound subscription host overrides)\n│   │   │   ├── server.go       #   /panel/api/server  (status, xray version, certs, logs, DB import/export)\n│   │   │   ├── setting.go      #   /panel/api/setting (settings + API tokens)\n│   │   │   ├── xray_setting.go #   /panel/api/xray    (raw Xray config editor, WARP/Nord/PIA, geodata)\n│   │   │   ├── api.go          #   /panel/api gateway (token auth, envelope + CSRF wiring)\n│   │   │   ├── index.go        #   login/logout/csrf/2FA\n│   │   │   ├── spa.go          #   SPA fallback for /panel UI routes\n│   │   │   └── websocket.go    #   WS upgrade endpoint\n│   │   ├── service/            # ⭐⭐ Business logic. This is where most real work happens.\n│   │   │   ├── inbound.go              # Inbound CRUD core (~1.4k lines)\n│   │   │   ├── inbound_node.go         # ⭐ Node sync for inbounds: reconcile, traffic merge (~1.1k lines)\n│   │   │   ├── inbound_traffic.go      # Per-client traffic accounting (~1.1k lines)\n│   │   │   ├── inbound_clients.go      # Client-within-inbound operations\n│   │   │   ├── inbound_sublink.go      # Inbound-level subscription link helpers\n│   │   │   ├── inbound_migration.go    # Inbound schema/format migrations\n│   │   │   ├── client_crud.go          # Client create/read/update/delete\n│   │   │   ├── client_bulk.go          # Bulk client ops (~1.6k lines)\n│   │   │   ├── client_inbound_apply.go # ⭐ Apply client changes to runtime (Local/Remote) (~1.2k lines)\n│   │   │   ├── client_groups.go        # Client grouping\n│   │   │   ├── client_link.go          # Per-client share-link generation\n│   │   │   ├── client_external_link.go # External links attached to clients\n│   │   │   ├── client_wireguard.go     # WireGuard client specifics\n│   │   │   ├── client_paging.go        # Server-side pagination/sort/filter for client lists\n│   │   │   ├── node.go                 # ⭐ NodeService: CRUD, probe, heartbeat, dirty-tracking (~1.1k lines)\n│   │   │   ├── node_mtls.go            # Node mTLS certificate management (master side)\n│   │   │   ├── node_tree.go            # Node hierarchy / descendants\n│   │   │   ├── host.go                 # Host rows (subscription output overrides)\n│   │   │   ├── server.go               # ServerService: status, certs, xray install, DB ops (~2.2k lines)\n│   │   │   ├── setting.go              # SettingService: all panel settings + defaults (~1.3k lines)\n│   │   │   ├── setting_mtls.go         # mTLS settings (node hardening)\n│   │   │   ├── traffic_writer.go       # Batched persistence of traffic deltas to the DB\n│   │   │   ├── xray.go                 # ⭐ XrayService: config gen + restart/hot-apply (~1.2k lines)\n│   │   │   ├── xray_setting.go         # Raw Xray config persistence\n│   │   │   ├── geodata.go              # Geo database browsing + routing-token validation\n│   │   │   ├── xray_metrics.go         # Xray observability metrics\n│   │   │   ├── metric_history.go       # Historical system/xray metrics\n│   │   │   ├── reality_scan.go         # REALITY target scanner\n│   │   │   ├── url_safety.go           # Outbound URL validation (SSRF guards)\n│   │   │   ├── outbound_subscription.go# Outbound subscription (e.g. Warp/Nord provider configs)\n│   │   │   ├── port_conflict.go        # Detect inbound port collisions\n│   │   │   ├── fallback.go             # Xray fallback (SNI/ALPN routing on shared port)\n│   │   │   ├── email/                  # Email notification service (SMTP)\n│   │   │   ├── integration/            # External providers: warp.go, nord.go, pia.go\n│   │   │   ├── outbound/               # Outbound config service\n│   │   │   ├── panel/                  # Cross-cutting panel services:\n│   │   │   │   ├── panel.go            #   panel-level helpers\n│   │   │   │   ├── user.go             #   admin user auth (bcrypt)\n│   │   │   │   ├── api_token.go        #   API token CRUD (SHA-256 hashed)\n│   │   │   │   └── websocket.go        #   WS hub / push service\n│   │   │   ├── discord/                # Discord bot client, Gateway v10, and subscriber\n│   │   │   └── tgbot/                  # Telegram bot command handlers\n│   │   ├── runtime/            # ⭐⭐ The Local/Remote node abstraction (see §5.2)\n│   │   │   ├── runtime.go      #   the Runtime interface (the contract)\n│   │   │   ├── local.go        #   Local impl → this box's Xray gRPC API\n│   │   │   ├── remote.go       #   Remote impl → HTTPS calls to a child node\n│   │   │   ├── tls_client.go   #   per-node HTTP client: verify / skip / pin / mtls\n│   │   │   └── manager.go      #   RuntimeFor(nodeID) → picks Local or Remote\n│   │   ├── job/               # Cron job structs (one file per job — see §5.4)\n│   │   ├── middleware/        # Gin middleware: security.go (headers/HSTS), bodylimit.go,\n│   │   │                      #   domainValidator.go, validate.go (CSRF), config_envelope.go\n│   │   ├── global/           # Global singletons: web server + sub server handles, restart hook\n│   │   ├── network/          # Custom net listeners (e.g. proxy-protocol aware)\n│   │   ├── session/          # Session/cookie helpers\n│   │   ├── websocket/        # WS hub implementation\n│   │   ├── locale/ + translation/  # i18n middleware + 13 locale JSON catalogs\n│   │   ├── entity/           # Shared request/response DTOs\n│   │   └── dist/             # ⚠️ Vite build output, embedded via go:embed (generated — do not hand-edit)\n│   │\n│   ├── sub/                   # The subscription server (separate from panel)\n│   │   ├── sub.go             #   server bootstrap\n│   │   ├── controller.go      #   routes for raw / JSON / Clash subscription formats\n│   │   ├── service.go         # ⭐ The link/config builder (~2.5k lines — share-link logic lives here)\n│   │   ├── json_service.go    #   JSON subscription format\n│   │   ├── clash_service.go   #   Clash/Mihomo YAML format\n│   │   ├── clash_external.go  #   external Clash config integration\n│   │   ├── external_subscription.go / external_config.go  # external sub import/aggregation\n│   │   ├── host_sub.go        #   Host-row overrides applied to subscription output\n│   │   ├── endpoint.go        #   subscription endpoint configuration\n│   │   ├── vless_route.go     #   VLESS route shaping\n│   │   ├── remark_vars.go     #   remark variable expansion\n│   │   └── links.go           #   link helpers\n│   │\n│   ├── mtproto/              # Embedded MTProto (Telegram) proxy: manager.go + per-OS\n│   │                         #   process supervision + orphan cleanup\n│   ├── logger/              # App logger (op/go-logging + lumberjack rotation)\n│   └── util/                # Leaf helpers (no business logic):\n│       ├── common/          #   errors, misc\n│       ├── crypto/          #   key/cert generation (x25519, ML-KEM/ML-DSA, ECH)\n│       ├── link/            #   outbound share-link building primitives\n│       ├── wirecodec/ + wireguard/  # WireGuard codec + integration helpers\n│       └── random/, json_util/, reflect_util/, sys/, netproxy/, netsafe/, ldap/\n│\n├── frontend/                 # React SPA (built into internal/web/dist)\n│   ├── vite.config.js        # ⭐ Build config: outDir → ../internal/web/dist, dev on :5173\n│   │                         #   (strict) proxying to :2053, entries index/login/subpage.html\n│   ├── package.json          # scripts: dev / build / preview / lint / typecheck / test / gen\n│   └── src/\n│       ├── main.tsx / routes.tsx / queryClient.ts   # SPA entry, router, query client\n│       ├── entries/          # Extra HTML entry points: login.tsx, subpage.tsx\n│       ├── pages/            # ⭐ Route screens. Mirrors the panel's feature areas:\n│       │   ├── inbounds/     #   inbound list + the big inbound form (protocols/security/transport)\n│       │   ├── clients/      #   client management screens\n│       │   ├── nodes/        #   multi-node UI\n│       │   ├── hosts/        #   subscription host-override UI\n│       │   ├── xray/         #   raw Xray config UI (routing, dns, outbounds, balancers, overrides)\n│       │   ├── index/        #   dashboard/home\n│       │   └── settings/, groups/, sub/, login/, api-docs/\n│       ├── api/              # ⭐ Data layer: http-init, QueryProvider, queryKeys, websocket bridge\n│       │   └── queries/      #   TanStack Query hooks (useNodesQuery, useStatusQuery, …)\n│       ├── schemas/          # Zod schemas: protocols, forms, api, primitives\n│       ├── generated/        # ⚠️ GENERATED from Go (see §5.5): schemas.ts, types.ts, zod.ts, examples.ts\n│       ├── components/       # Reusable UI (clients/ form/ geodata/ ui/ viz/ feedback/ utility/)\n│       ├── lib/              # Frontend domain logic (xray/ inbounds/ clients/)\n│       ├── hooks/, models/, layouts/, i18n/, utils/, styles/\n│       └── test/             # Vitest + golden fixtures (config-generation snapshot tests)\n│\n├── tools/openapigen/         # ⭐ Go program that emits frontend/src/generated/* from Go types (§5.5)\n├── docs/                     # Markdown docs (this file, custom-subscription-templates.md, …)\n├── media/                    # README images\n│\n├── Dockerfile / docker-compose.yml / DockerEntrypoint.sh / DockerInit.sh   # Container build/run\n├── install.sh / update.sh / x-ui.sh                        # VPS install + management CLI\n├── x-ui.service.*  / x-ui.rc                               # systemd units (debian/rhel/arch) + rc script\n├── windows_files/                                          # Windows service support\n└── .github/workflows/        # CI: ci.yml, codeql.yml, docker.yml, release.yml,\n                              #     mutation.yml, cleanup_caches.yml, claude-pr-review.yml,\n                              #     claude-issue-analyst.yml\n```\n\n---\n\n## 5. Cross-cutting subsystems (the parts that span many files)\n\n### 5.1 DB → Xray config pipeline (config generation & application)\n\nThe panel never edits Xray's running config directly from controllers. The flow is:\n\n1. A service mutates DB state (inbound/client/setting).\n2. `XrayService` (`service/xray.go`) builds a fresh `xray.Config` from DB state\n   (`GetXrayConfig`).\n3. It tries a **hot apply** (`tryHotApply` → `xray/hot_diff.go`): diff old vs new config and\n   push only the deltas over the Xray gRPC API (add/remove inbound, add/remove user) — **no\n   process restart**, so live connections survive.\n4. If the diff isn't hot-applicable (structural change), it falls back to a **full restart**\n   of the Xray process (`xray/process.go`).\n\nRestart is debounced via an atomic \"need restart\" flag (`SetToNeedRestart` /\n`IsNeedRestartAndSetFalse`), consumed by a `@every 30s` cron task registered in `startTask()`\n— any number of mutations inside the window causes at most one restart.\n\n**Key files:** `service/xray.go` (orchestration), `xray/hot_diff.go` (the diff algorithm),\n`xray/process.go` (process lifecycle), `xray/api.go` (gRPC calls), `xray/config.go` (config model).\n\n### 5.2 Runtime abstraction — Local vs Remote (multi-node) ⭐ most important\n\nA \"node\" (`model.Node`) is another 3x-ui instance this panel controls. Every state-changing\ninbound/client operation goes through the `runtime.Runtime` interface so the _same service\ncode_ works whether the target is the local Xray or a remote node.\n\n- **Interface:** `internal/web/runtime/runtime.go` — `Name`, `AddInbound`, `DelInbound`,\n  `UpdateInbound`, `AddUser`, `RemoveUser`, `UpdateUser`, `DeleteUser`, `AddClient`,\n  `RestartXray`, `ResetClientTraffic`, `ResetInboundTraffic`, `ResetAllTraffics`.\n- **`Local`** (`local.go`): calls this box's Xray gRPC API directly.\n- **`Remote`** (`remote.go`): serializes the operation and sends it over HTTPS to the child\n  node's API.\n- **TLS modes** (`tls_client.go`, per-node `TlsVerifyMode`):\n  `verify` (system CAs, default) / `skip` (no validation) / `pin` (leaf cert SHA-256 must\n  match `PinnedCertSha256`) / `mtls` (master presents a client certificate; node cert checked\n  against system roots; API token optional). Master-side cert management:\n  `service/node_mtls.go` + `service/setting_mtls.go`.\n- **Dispatch:** `manager.go` → `Manager.RuntimeFor(nodeID *int)`; `nil` nodeID → `Local`,\n  otherwise a cached/lazy-loaded `Remote`. `InvalidateNode(id)` drops a cached remote client.\n\nBearer tokens use the narrowest scope that matches the caller's duties:\n\n| Scope        | Access                                                                 |\n| ------------ | ---------------------------------------------------------------------- |\n| `monitor`    | Read-only status, version, and metric endpoints.                       |\n| `node-sync`  | Node configuration, inbound/client synchronization, traffic, and Xray. |\n| `node-admin` | All `node-sync` access plus `POST /panel/api/server/updatePanel`.      |\n| `admin`      | Every panel API endpoint; intended for fully trusted automation.       |\n\n`node-admin` remains a node synchronization identity when inbound rules are applied. This\npreserves the node-specific validation path while keeping software-update authority separate\nfrom ordinary `node-sync` credentials. On an mTLS connection, only a valid `node-admin` bearer\nelevates the request; other bearer scopes retain the historical mTLS `node-sync` behavior.\nExisting tokens retain their stored scope, and legacy tokens without one continue to migrate to\n`admin`.\n\n**Node identity & attribution (the hard part).** Inbounds carry a `NodeID` _and_ an\n`OriginNodeGuid`. Because inbounds can be pushed across hops, the panel attributes traffic and\nonline clients back to the originating panel using **stable GUIDs** rather than local IDs.\nRelevant logic: `service/inbound_node.go` (`ReconcileNode`, `SetRemoteTraffic`, GUID merge,\n`synthNodeGuid`, `panelGuid`) and `service/node.go` (`effectiveNodeGuid`, heartbeat, dirty\ntracking). Node \"dirty\" flags drive an **anti-entropy reconciliation** so an offline node's\ninbound edits converge once it reconnects.\n\n**Where to look for node bugs:**\n\n- Operation not reaching a node → `runtime/remote.go` + `runtime/manager.go`.\n- Wrong traffic/online attribution across hops → `service/inbound_node.go` (GUID merge paths).\n- Node shown offline / stale status → `job/node_heartbeat_job.go` + `service/node.go` (`Probe`, `UpdateHeartbeat`).\n- Edits to an offline node not applying on reconnect → dirty/reconcile logic in `service/inbound_node.go` + `service/node.go` (`MarkNodeDirty`/`ClearNodeDirty`/`NodeSyncState`).\n- TLS/mTLS handshake failures → `runtime/tls_client.go`, `service/node_mtls.go`, `service/node.go` (`FetchCertFingerprint`).\n\n### 5.3 Traffic accounting\n\nPer-client and per-inbound up/down counters originate from Xray's stats API and are persisted\nto the DB. The Xray traffic job polls the core; node traffic is pulled from child nodes and\nmerged with GUID-based baselines to avoid double counting after resets.\n\n**Key files:** `service/inbound_traffic.go`, `service/traffic_writer.go`,\n`job/xray_traffic_job.go`, `job/node_traffic_sync_job.go`, `service/inbound_node.go`\n(`SetRemoteTraffic` / `upsertNodeBaseline`), models `xray.ClientTraffic`,\n`model.NodeClientTraffic`, `model.ClientGlobalTraffic` (cross-master totals).\nA client reset is queued per hosting node in `model.NodePendingReset` (`service/node_reset_queue.go`)\nand replayed by the node sync until the node accepts it.\nPeriodic resets: `job/periodic_traffic_reset_job.go` (keyed off `Inbound.TrafficReset`).\n\n### 5.4 Background jobs (cron)\n\nAll registered in `web.go` → `startTask()`. Each is a struct with a `Run()` method in `internal/web/job/`:\n\n| Schedule            | Job                                                                                              | Purpose / condition                                                                   |\n| ------------------- | ------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------- |\n| `@every 1s`         | `check_xray_running_job`                                                                         | Restart Xray if it died (2 consecutive down checks)                                   |\n| `@every 30s`        | (inline func in `startTask`)                                                                     | Debounced Xray restart — consumes the \"need restart\" flag (§5.1)                      |\n| `@every 5s`         | `xray_traffic_job`                                                                               | Pull traffic stats from Xray (5s start delay)                                         |\n| `@every 5s`         | `node_heartbeat_job`                                                                             | Probe child nodes (online/offline)                                                    |\n| `@every 5s`         | `node_traffic_sync_job`                                                                          | Pull + merge node traffic; push reconciliation                                        |\n| `@every 10s`        | `check_client_ip_job`                                                                            | Enforce per-client IP limits                                                          |\n| `@every 10s`        | `mtproto_job`                                                                                    | Reconcile `mtg` sidecars against enabled MTProto inbounds                             |\n| `@every 10s`        | `amneziawg_job`                                                                                  | Reconcile embedded AmneziaWG interfaces against enabled local inbounds                |\n| `@every 5m`         | `outbound_subscription_job`                                                                      | Refresh outbound provider configs                                                     |\n| `@every 10m`        | `clear_logs_job` (`PruneXrayLogsJob`)                                                            | Truncate Xray access/error logs once either exceeds 64 MiB                            |\n| `@hourly`           | `warp_ip_job`, `periodic_traffic_reset_job(\"hourly\")`                                            | WARP IP rotation; traffic resets                                                      |\n| `@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       |\n| `@weekly`           | `periodic_traffic_reset_job(\"weekly\")`                                                           | Weekly traffic resets                                                                 |\n| default `@every 1m` | `ldap_sync_job`                                                                                  | Only if LDAP enabled; schedule configurable                                           |\n| default `@daily`    | `stats_notify_job`                                                                               | Only if TG bot enabled; schedule configurable                                         |\n| default `@daily`    | `discord_notify_job`                                                                             | Only if Discord bot enabled; schedule configurable                                    |\n| `@every 2m`         | `check_hash_storage`                                                                             | Only if TG bot enabled; expires bot callback hashes                                   |\n| `@every 1m`         | `check_cpu_usage`                                                                                | Only if a CPU alarm is configured (TG, Discord, or email); publishes `cpu.high`       |\n| `@every 1m`         | `check_memory_usage`                                                                             | Only if a memory alarm is configured (TG, Discord, or email); publishes `memory.high` |\n| configurable        | `free_os_memory`                                                                                 | Only if `sys.MemoryReleaseIntervalMinutes() > 0`; returns heap to OS                  |\n\nTo change _when_ something runs, edit `startTask()`. To change _what_ it does, edit the job file.\n\n### 5.5 Type generation (Go → TypeScript) ⚠️ don't hand-edit generated files\n\nThe Go backend is the schema source of truth. `tools/openapigen` (a Go program, with a\n`StructAllow` allowlist of exported types) emits\n`frontend/src/generated/{schemas,types,zod,examples}.ts`. The frontend build runs this first:\n\n- `npm run gen:zod` → `go run ./tools/openapigen` (regenerate from Go)\n- `npm run gen:api` → builds the OpenAPI doc (`scripts/build-openapi.mjs`, driven by the\n  hand-maintained endpoint registry `src/pages/api-docs/endpoints.ts`)\n- `npm run build` runs `gen:api` then `vite build`.\n\n**Implication:** if you change a Go model/DTO that crosses the API boundary, regenerate the\nfrontend types (`cd frontend && npm run gen`) instead of editing `src/generated/` by hand.\n\n### 5.6 Share-link / subscription generation\n\nTwo distinct code paths produce client configs:\n\n- **Per-client links in the panel** (the \"copy link\" / QR in the UI): `service/client_link.go`\n  - `util/link/outbound.go`.\n- **Subscription endpoint** (what a client app polls): `internal/sub/service.go` (raw links),\n  `internal/sub/json_service.go` (JSON), `internal/sub/clash_service.go` (Clash YAML).\n  **`Host` rows** (`model.Host`, edited under /panel/api/hosts) override address/SNI/path/\n  security per inbound in subscription output — applied in `sub/host_sub.go`.\n\nBoth paths must agree per protocol. A malformed link for a specific protocol/transport combo\n(e.g. XHTTP + Reality) is usually a field-lookup mismatch in **`internal/sub/service.go`** (and\nits tests `service_test.go` / golden fixtures), or in `util/link/outbound.go`. The frontend\nalso has protocol schemas under `frontend/src/schemas/protocols/` and `frontend/src/lib/xray/`.\n\n### 5.7 Event bus (in-process pub/sub)\n\n`internal/eventbus/` is a minimal buffered-channel pub/sub. Producers call a non-blocking\n`Publish(Event)`; all subscribers receive every event. Event types: `outbound.down|up`,\n`xray.crash`, `node.down|up`, `cpu.high`, `memory.high`, `login.attempt`, with structured\npayloads (OutboundHealthData, NodeHealthData, LoginEventData, SystemMetricData). Producers\ninclude the CPU/memory jobs, node heartbeat, and login handling; consumers include the\nTelegram bot, the Discord bot (`service/discord/`), and the email notifier (`service/email/`). Use it for cross-cutting\nnotifications instead of importing notification services into producers.\n\n### 5.8 Tunnel health monitor\n\n`internal/tunnelmonitor/` is an optional watchdog configured **only via env vars**\n(`XUI_TUNNEL_HEALTH_*`, read in `internal/config/`), deliberately independent of panel\nsettings so it can be enabled from a systemd `EnvironmentFile` even when the panel is\nunreachable. It periodically probes an HTTP URL (default: Cloudflare trace endpoint) through\nthe tunnel; after N successive failures (default 3) it fires a recovery callback wired to an\nXray restart.\n\n---\n\n## 6. Data model cheat-sheet\n\nGORM models in `internal/database/model/` (main file `model.go` + siblings); all registered\nfor AutoMigrate in `internal/database/db.go`.\n\n| Model                           | Table role                                | Notable fields                                                                                                                                                     |\n| ------------------------------- | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| `User`                          | Admin login                               | bcrypt password, `LoginEpoch` (invalidates sessions)                                                                                                               |\n| `Inbound`                       | An Xray inbound                           | `Tag` (unique), `Port`, `Protocol`, `Settings`/`StreamSettings`/`Sniffing` (JSON), `Enable`, `TrafficReset`, `NodeID`, **`OriginNodeGuid`**, `ClientStats` (assoc) |\n| `Client`                        | In-memory client view                     | UUID/email/flow/limits (parsed from inbound JSON; not persisted)                                                                                                   |\n| `ClientRecord`                  | Persisted client (`clients`)              | `Email` (unique), `SubID`, `UUID`, `TotalGB`, `ExpiryTime`, `LimitIP`, `Group`, `Reset`                                                                            |\n| `ClientGroup` / `ClientInbound` | Grouping + client↔inbound join            | many-to-many wiring, `FlowOverride`                                                                                                                                |\n| `ClientExternalLink`            | Extra links attached to a client          | `Kind`, `Value`, `Remark`, `SortIndex`                                                                                                                             |\n| `Host`                          | Subscription host overrides (per inbound) | `Address`, `Port`, `Sni`, `Path`, `Security`, `Fingerprint`, `SortOrder`, visibility/exclusion flags                                                               |\n| `Node`                          | A managed child panel                     | `Guid`, `Address`, `Status`, `TlsVerifyMode`, `PinnedCertSha256`, `ConfigDirty`, version/heartbeat/metric fields                                                   |\n| `NodeClientTraffic`             | Per-node client traffic baseline          | cross-node merge (anti-double-count)                                                                                                                               |\n| `NodePendingReset`              | Client resets a node has not confirmed    | `NodeId`, `Email`, `QueuedAt`; replayed by the node sync, freezes that client's node verdict until delivered                                                       |\n| `NodeClientIp`                  | Per-node client IP attribution            | `NodeGuid`, `Email`, `Ips`                                                                                                                                         |\n| `ClientGlobalTraffic`           | Cross-master usage totals                 | `MasterGuid`, `Email`, `Up`, `Down`                                                                                                                                |\n| `xray.ClientTraffic`            | Per-client counters (`client_traffics`)   | `Email`, `Up`, `Down`, `Total`, `ExpiryTime`, `LastOnline`                                                                                                         |\n| `InboundClientIps`              | IP set per client email                   | drives IP-limit enforcement                                                                                                                                        |\n| `OutboundTraffics`              | Outbound counters                         | per outbound tag                                                                                                                                                   |\n| `OutboundSubscription`          | External provider subs                    | Warp/Nord style                                                                                                                                                    |\n| `Setting`                       | Key/value panel settings                  | everything configurable                                                                                                                                            |\n| `ApiToken`                      | REST API tokens                           | SHA-256 hash (plaintext shown once)                                                                                                                                |\n| `InboundFallback`               | Fallback routing on a shared port         | SNI/ALPN/path → dest                                                                                                                                               |\n| `HistoryOfSeeders`              | Seeder bookkeeping                        | prevents re-running one-off migrations                                                                                                                             |\n\n---\n\n## 7. Symptom → File index (start here when debugging)\n\n| Symptom / task                                                                    | Primary file(s)                                                              | Then check                                                                                          |\n| --------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- |\n| 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`                            |\n| **Inbound** create/update/delete behavior                                         | `service/inbound.go`, `service/inbound_clients.go`                           | `runtime/*`, `service/xray.go`                                                                      |\n| **Client** CRUD / limits / expiry                                                 | `service/client_crud.go`, `service/client_inbound_apply.go`                  | model `ClientRecord`, `service/inbound_traffic.go`                                                  |\n| **Bulk** client operations slow/wrong                                             | `service/client_bulk.go`                                                     | `service/client_paging.go`                                                                          |\n| Xray **won't apply** a config change                                              | `service/xray.go` (`RestartXray`, `tryHotApply`)                             | `xray/hot_diff.go`, `xray/process.go`                                                               |\n| Xray **restarts when it shouldn't** (kills connections)                           | `xray/hot_diff.go` (diff not classified as hot)                              | `service/xray.go`                                                                                   |\n| **Traffic** counts wrong / reset behavior                                         | `service/inbound_traffic.go`, `job/xray_traffic_job.go`                      | `service/traffic_writer.go`, `job/periodic_traffic_reset_job.go`                                    |\n| **Node** operation not propagating                                                | `runtime/remote.go`, `runtime/manager.go`                                    | `service/inbound_node.go`                                                                           |\n| **Multi-hop / cross-node attribution** (traffic or online clients on wrong panel) | `service/inbound_node.go` (GUID merge, `synthNodeGuid`, `effectiveNodeGuid`) | `service/node.go`, model `OriginNodeGuid`/`Node.Guid`                                               |\n| Node stuck **offline / stale**                                                    | `job/node_heartbeat_job.go`, `service/node.go` (`Probe`, `UpdateHeartbeat`)  | `runtime/tls_client.go` (TLS verify)                                                                |\n| Node **TLS / mTLS** auth failures                                                 | `runtime/tls_client.go`, `service/node_mtls.go`, `service/setting_mtls.go`   | `service/node.go` (`FetchCertFingerprint`)                                                          |\n| Offline node edits **not reconciling** on reconnect                               | `service/inbound_node.go` (`ReconcileNode`, dirty flags)                     | `service/node.go` (`MarkNodeDirty`/`NodeSyncState`)                                                 |\n| **Share link / QR** malformed (per protocol)                                      | `service/client_link.go`, `util/link/outbound.go`                            | `frontend/src/lib/xray/`, `frontend/src/schemas/protocols/`                                         |\n| **Subscription** output wrong (raw/JSON/Clash)                                    | `internal/sub/service.go`                                                    | `sub/json_service.go`, `sub/clash_service.go`, sub golden tests                                     |\n| Subscription **host overrides** not applied                                       | `service/host.go`, `sub/host_sub.go`                                         | model `Host`, `frontend/src/pages/hosts/`                                                           |\n| **External subscription** import/aggregation                                      | `sub/external_subscription.go`, `sub/external_config.go`                     | `sub/clash_external.go`                                                                             |\n| **Settings** not saving / defaults                                                | `service/setting.go`, `controller/setting.go`                                | model `Setting`                                                                                     |\n| **Login / 2FA / sessions / CSRF**                                                 | `controller/index.go`, `service/panel/user.go`, `middleware/`                | `session/`                                                                                          |\n| **API tokens**                                                                    | `service/panel/api_token.go`, `controller/setting.go`                        | model `ApiToken`                                                                                    |\n| **Port conflict** on inbound add                                                  | `service/port_conflict.go`                                                   | `controller/inbound.go`                                                                             |\n| **Fallbacks** (shared 443, SNI routing)                                           | `service/fallback.go`, `controller/inbound.go`                               | model `InboundFallback`                                                                             |\n| **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()` |\n| **`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/`                            |\n| **Telegram bot** commands                                                         | `service/tgbot/`                                                             | `job/stats_notify_job.go`                                                                           |\n| **Discord bot** commands & reports                                                | `service/discord/`                                                           | `job/discord_notify_job.go`                                                                         |\n| **Email notifications**                                                           | `service/email/`                                                             | `internal/eventbus/` (consumers)                                                                    |\n| **CPU / memory alerts** not firing                                                | `job/check_cpu_usage.go`, `job/check_memory_usage.go`                        | `internal/eventbus/`, notifier settings in `service/setting.go`                                     |\n| Xray auto-restart on **dead tunnel**                                              | `internal/tunnelmonitor/`                                                    | `XUI_TUNNEL_HEALTH_*` in `internal/config/`                                                         |\n| **WARP / Nord / PIA** outbound integration                                        | `service/integration/warp.go` / `nord.go` / `pia.go`                         | `internal/pia/`, `frontend/src/pages/xray/overrides/`                                               |\n| **MTProto** proxy issues                                                          | `internal/mtproto/manager.go`, `mtproto/process*.go`                         | `job/mtproto_job.go`                                                                                |\n| **DB migration** / new column                                                     | `internal/database/db.go` (AutoMigrate list), `migrate_data.go`              | `model/model.go`                                                                                    |\n| **Cron schedule** changes                                                         | `web.go` → `startTask()`                                                     | the specific `job/*.go`                                                                             |\n| **CORS / security headers / HTTPS**                                               | `middleware/`, `web.go` (`initRouter`, TLS setup)                            | `config/` (env)                                                                                     |\n| **Env vars / paths / DB type**                                                    | `internal/config/config.go`                                                  | `.env.example`                                                                                      |\n| **Frontend route / screen**                                                       | `frontend/src/pages/<area>/`, `frontend/src/routes.tsx`                      | `frontend/src/api/queries/`                                                                         |\n| **Frontend ↔ backend type mismatch**                                              | regenerate: `cd frontend && npm run gen` (`tools/openapigen`)                | `frontend/src/generated/`                                                                           |\n| **System status / CPU / metrics**                                                 | `service/server.go`, `service/xray_metrics.go`, `service/metric_history.go`  | `controller/server.go`, gopsutil                                                                    |\n\n---\n\n## 8. Layering rules (where new code belongs)\n\n1. **Controllers are thin.** Only: bind/validate input, call one service, shape the HTTP\n   response. No DB queries, no Xray calls, no business rules in `controller/`.\n2. **Services own the logic and transactions.** All business rules, DB access, and decisions\n   about applying changes live in `service/`. If you're tempted to query GORM from a\n   controller, move it to a service.\n3. **Never touch Xray's running state from a controller or job directly.** Go through\n   `XrayService` / the `runtime.Runtime` interface so local vs node dispatch stays correct.\n4. **Any state-changing inbound/client o"},"items":[{"name":"architecture.md","path":"docs/architecture.md","rawUrl":"https://raw.githubusercontent.com/MHSanaei/3x-ui/HEAD/docs/architecture.md","title":"docs - System Architecture & Module Boundaries","category":"project-spec","format":"markdown","content":"# 3x-ui — Architecture & Code Map\n\n> Navigation map for contributors and AI coding agents (referenced from `CLAUDE.md`).\n> Goal: jump to the right file in one hop instead of grepping the whole tree.\n> Tracks the `main` branch — paths reflect the latest changes, so verify against the live\n> tree rather than a pinned release (Go module `github.com/mhsanaei/3x-ui/v3`).\n>\n> **How to use this file:** read \"Mental model\" + \"Request lifecycle\" first, then\n> use the **Symptom → File index** to locate work. Respect the **Layering rules**\n> when adding code. Verify with the commands in **Build / Test / Lint**.\n\n---\n\n## 1. Mental model (the 30-second version)\n\n3x-ui is a **web control panel for [Xray-core](https://github.com/XTLS/Xray-core)**. The Go\nbackend is the source of truth: it stores inbounds/clients/settings in a DB, renders an\nXray JSON config from that state, supervises the Xray child process, and exposes a REST +\nWebSocket API. A React SPA (built by Vite, embedded into the Go binary) is the UI. A second,\nseparate HTTP server serves **subscription links** to end users.\n\nThe panel supervises **managed child processes**: Xray-core itself and — when MTProto\ninbounds exist — a dedicated child proxy binary:\n\n- **`mtg-multi` for MTProto inbounds** (`github.com/mhsanaei/mtg-multi`, a multi-secret fork\n  built from source; `internal/mtproto/`): One process per inbound serves every attached\n  client's FakeTLS secret through the fork's `[secrets]` section, plus optional per-client\n  sponsored-channel ad-tags via `[secret-ad-tags]`. A client or ad-tag edit is hot-applied via\n  the fork's management API (`PUT /secrets`, guarded by a per-process bearer token), with a\n  process restart as the fallback on older binaries.\n\nIn contrast, **AmneziaWG** (`internal/amneziawgnet/`) and **TUIC v5** (`internal/tuic/`) run as\n**in-process native Go servers** without external child processes, bridging client traffic into\nXray-core via loopback SOCKS5 relays.\n\nServers and processes, all launched from `main.go`:\n\n| Server / process | Package                           | Purpose                                                            | Default port      |\n| ---------------- | --------------------------------- | ------------------------------------------------------------------ | ----------------- |\n| **Panel**        | `internal/web`                    | Admin REST/WS API + serves the embedded SPA                        | 2053              |\n| **Subscription** | `internal/sub`                    | Public endpoint that hands out client configs (raw / JSON / Clash) | `subPort` setting |\n| **Xray-core**    | supervised via `internal/xray`    | The actual proxy engine; a child process, not Go code              | `inbounds[].port` |\n| **mtg-multi**    | supervised via `internal/mtproto` | MTProto proxy child process for MTProto inbounds (multi-secret)    | per inbound       |\n\nTwo key ideas that explain most of the complexity:\n\n1. **The DB → Xray config pipeline.** Inbounds/clients live in the DB. On every change the\n   backend regenerates the Xray config and applies it — preferring a _hot diff_ (live gRPC\n   API mutation) over a full process restart. See §5.1.\n2. **The Runtime abstraction (multi-node).** A panel can manage remote \"nodes\" (other 3x-ui\n   instances). Every state-changing inbound/client operation is dispatched through a\n   `runtime.Runtime` interface that is either **`Local`** (this box's Xray gRPC API) or\n   **`Remote`** (HTTPS call to a child node, with `verify`/`skip`/`pin`/`mtls` TLS modes).\n   This is the single most important abstraction in the project. See §5.2.\n\n---\n\n## 2. Tech stack\n\n**Backend (Go 1.27):**\n\n- Web framework: **Gin** (`gin-gonic/gin`) + sessions (cookie store), gzip.\n- ORM: **GORM** with **SQLite** (default) or **PostgreSQL** (`XUI_DB_TYPE=postgres`).\n- Scheduler: **robfig/cron/v3** (seconds-precision) for all background jobs.\n- Xray: **xtls/xray-core** vendored as a library; the panel talks to the running core over\n  its **gRPC API** and also shells out to manage the process.\n- Bots: Telegram bot (**mymmrac/telego**), Discord bot (Discord REST API v10 + **gorilla/websocket** Gateway v10). i18n: **nicksnyder/go-i18n**.\n- Misc: gorilla/websocket, gopsutil (system stats), go-qrcode, gotp (2FA TOTP).\n\n**Frontend (`frontend/`):**\n\n- **React 19** + **Ant Design 6** + **Vite 8** + **TypeScript**.\n- Data layer: **TanStack Query** (`@tanstack/react-query`) over the native **Fetch API**; **Zod 4** schemas.\n- Router: **react-router 8**. Charts: **uPlot** (`frontend/src/components/viz/Sparkline.tsx`). Editor: **CodeMirror 6**.\n- **Build output goes to `internal/web/dist/`** (see `vite.config.js` → `outDir`) and is\n  embedded into the Go binary with `go:embed`. Three HTML entries: `index.html` (panel SPA),\n  `login.html`, `subpage.html`. The Go server serves the SPA; there is no separate frontend\n  deployment.\n\n**Important:** the legacy Go-template UI and `web/assets/` are **gone**. All HTML/JS comes\nfrom the embedded Vite `dist/`. Don't look for `.html` templates in `internal/web`.\n\n---\n\n## 3. Request lifecycle (follow the data)\n\n### 3.1 Admin API request (e.g. \"add a client\")\n\n```\nBrowser (React, fetch)\n  → POST {basePath}/panel/api/...\n    → Gin engine (internal/web/web.go: initRouter)\n      → middleware chain: SecurityHeaders → MaxBodyBytes (10 MiB; importDB exempt)\n                          → [DomainValidator, if webDomain set] → gzip → sessions(\"3x-ui\")\n                          → base-path/cache-control context → Localizer\n                          → API routes add: ConfigEnvelope (zstd + SHA-256) → CSRF\n        → Controller (internal/web/controller/*.go)   // HTTP concerns only: bind, validate, respond\n          → Service (internal/web/service/*.go)        // business logic + transactions\n            → GORM → DB (internal/database)            // persistence\n            → runtime.Runtime dispatch                 // apply to Xray (Local) or node (Remote)\n              → Local:  internal/xray (gRPC API or config regen + restart)\n              → Remote: internal/web/runtime/remote.go → HTTPS → child node's API\n```\n\nThe controller layer is thin. **Business logic lives in services.** When something is wrong\nwith _behavior_, the bug is almost always in a service file, not a controller.\n\n### 3.2 Subscription request (end-user fetching their config)\n\n```\nEnd user → GET {subPath}/{subId}   (separate server, internal/sub)\n  → internal/sub/controller.go (routes: raw / JSON / Clash variants, feature-flagged)\n    → internal/sub/service.go (~2.5k lines — the link/config builder)\n      → reads inbounds+clients+hosts from DB, renders per-protocol share links /\n        Clash YAML / JSON (Host rows can override address/SNI/path per inbound)\n```\n\n### 3.3 Background work (cron jobs)\n\nScheduled in `internal/web/web.go` → `startTask()`. Each job is a struct in\n`internal/web/job/`. Examples: poll Xray traffic every 5s, check client IP limits every 10s,\nnode heartbeat every 5s, periodic traffic resets (hourly/daily/weekly/monthly). See §5.4.\n\n---\n\n## 4. Directory map (what lives where)\n\n```\n3x-ui/\n├── main.go                     # Entry point: CLI (run / migrate / migrate-db / setting / cert),\n│                               #   bootstrap, signal handling, restart loop\n├── go.mod / go.sum             # Go deps (module path ends in /v3)\n│\n├── internal/                   # ALL backend Go code (private packages)\n│   ├── config/                 # Env-var config: paths, DB kind/DSN, log level, version\n│   │                           #   Every XUI_* env var is read here (config.go)\n│   ├── database/\n│   │   ├── db.go               # InitDB: connect, AutoMigrate, seeders (~1.4k lines). DB hotspot.\n│   │   ├── migrate_data.go     # Data migrations (seeders/normalizers beyond AutoMigrate)\n│   │   ├── dialect.go          # SQLite vs Postgres SQL differences\n│   │   ├── dump_sqlite.go      # DB export/backup\n│   │   └── model/              # **ALL GORM models** (model.go ~1.1k lines + siblings:\n│   │                           #   node_client_traffic.go, node_client_ip.go,\n│   │                           #   client_global_traffic.go). ⭐ Start here for data shape.\n│   ├── pia/                    # PIA WireGuard protocol client (auth, signed server list, /addKey)\n│   ├── eventbus/               # In-process pub/sub (buffered channel): outbound.down|up,\n│   │                           #   xray.crash, node.down|up, cpu.high, memory.high, login.attempt\n│   ├── tunnelmonitor/          # Optional tunnel health probe (XUI_TUNNEL_HEALTH_* env vars):\n│   │                           #   HTTP probe (default Cloudflare trace); repeated failures\n│   │                           #   trigger an Xray restart hook. Independent of panel settings.\n│   ├── xray/                   # Xray-core integration (the proxy engine wrapper)\n│   │   ├── process.go          # Spawn/supervise the Xray child process (~750 lines)\n│   │   ├── api.go              # gRPC client to a running Xray (add/remove user, stats) (~800 lines)\n│   │   ├── hot_diff.go         # ⭐ Compute minimal live changes to avoid full restart (~500 lines)\n│   │   ├── config.go           # Xray config object model\n│   │   ├── inbound.go          # Inbound JSON shaping\n│   │   ├── client_traffic.go   # ClientTraffic model (persisted as client_traffics)\n│   │   ├── traffic.go          # Traffic type helpers\n│   │   ├── log_writer.go       # Pipe Xray stdout/stderr into the panel logger\n│   │   └── geodata/            # Browse geosite/geoip .dat: streaming protowire reader,\n│   │                           #   cached category index, routing-token parsing (token.go)\n│   │\n│   ├── web/                    # The panel server\n│   │   ├── web.go              # ⭐ Server bootstrap: initRouter (all routes) + startTask (all cron jobs)\n│   │   ├── controller/         # HTTP handlers (thin). One file per resource:\n│   │   │   ├── inbound.go      #   /panel/api/inbounds\n│   │   │   ├── client.go       #   /panel/api/clients (CRUD + bulk + ips + onlines)\n│   │   │   ├── group.go        #   client-group endpoints\n│   │   │   ├── node.go         #   /panel/api/nodes   (multi-node management)\n│   │   │   ├── host.go         #   /panel/api/hosts   (per-inbound subscription host overrides)\n│   │   │   ├── server.go       #   /panel/api/server  (status, xray version, certs, logs, DB import/export)\n│   │   │   ├── setting.go      #   /panel/api/setting (settings + API tokens)\n│   │   │   ├── xray_setting.go #   /panel/api/xray    (raw Xray config editor, WARP/Nord/PIA, geodata)\n│   │   │   ├── api.go          #   /panel/api gateway (token auth, envelope + CSRF wiring)\n│   │   │   ├── index.go        #   login/logout/csrf/2FA\n│   │   │   ├── spa.go          #   SPA fallback for /panel UI routes\n│   │   │   └── websocket.go    #   WS upgrade endpoint\n│   │   ├── service/            # ⭐⭐ Business logic. This is where most real work happens.\n│   │   │   ├── inbound.go              # Inbound CRUD core (~1.4k lines)\n│   │   │   ├── inbound_node.go         # ⭐ Node sync for inbounds: reconcile, traffic merge (~1.1k lines)\n│   │   │   ├── inbound_traffic.go      # Per-client traffic accounting (~1.1k lines)\n│   │   │   ├── inbound_clients.go      # Client-within-inbound operations\n│   │   │   ├── inbound_sublink.go      # Inbound-level subscription link helpers\n│   │   │   ├── inbound_migration.go    # Inbound schema/format migrations\n│   │   │   ├── client_crud.go          # Client create/read/update/delete\n│   │   │   ├── client_bulk.go          # Bulk client ops (~1.6k lines)\n│   │   │   ├── client_inbound_apply.go # ⭐ Apply client changes to runtime (Local/Remote) (~1.2k lines)\n│   │   │   ├── client_groups.go        # Client grouping\n│   │   │   ├── client_link.go          # Per-client share-link generation\n│   │   │   ├── client_external_link.go # External links attached to clients\n│   │   │   ├── client_wireguard.go     # WireGuard client specifics\n│   │   │   ├── client_paging.go        # Server-side pagination/sort/filter for client lists\n│   │   │   ├── node.go                 # ⭐ NodeService: CRUD, probe, heartbeat, dirty-tracking (~1.1k lines)\n│   │   │   ├── node_mtls.go            # Node mTLS certificate management (master side)\n│   │   │   ├── node_tree.go            # Node hierarchy / descendants\n│   │   │   ├── host.go                 # Host rows (subscription output overrides)\n│   │   │   ├── server.go               # ServerService: status, certs, xray install, DB ops (~2.2k lines)\n│   │   │   ├── setting.go              # SettingService: all panel settings + defaults (~1.3k lines)\n│   │   │   ├── setting_mtls.go         # mTLS settings (node hardening)\n│   │   │   ├── traffic_writer.go       # Batched persistence of traffic deltas to the DB\n│   │   │   ├── xray.go                 # ⭐ XrayService: config gen + restart/hot-apply (~1.2k lines)\n│   │   │   ├── xray_setting.go         # Raw Xray config persistence\n│   │   │   ├── geodata.go              # Geo database browsing + routing-token validation\n│   │   │   ├── xray_metrics.go         # Xray observability metrics\n│   │   │   ├── metric_history.go       # Historical system/xray metrics\n│   │   │   ├── reality_scan.go         # REALITY target scanner\n│   │   │   ├── url_safety.go           # Outbound URL validation (SSRF guards)\n│   │   │   ├── outbound_subscription.go# Outbound subscription (e.g. Warp/Nord provider configs)\n│   │   │   ├── port_conflict.go        # Detect inbound port collisions\n│   │   │   ├── fallback.go             # Xray fallback (SNI/ALPN routing on shared port)\n│   │   │   ├── email/                  # Email notification service (SMTP)\n│   │   │   ├── integration/            # External providers: warp.go, nord.go, pia.go\n│   │   │   ├── outbound/               # Outbound config service\n│   │   │   ├── panel/                  # Cross-cutting panel services:\n│   │   │   │   ├── panel.go            #   panel-level helpers\n│   │   │   │   ├── user.go             #   admin user auth (bcrypt)\n│   │   │   │   ├── api_token.go        #   API token CRUD (SHA-256 hashed)\n│   │   │   │   └── websocket.go        #   WS hub / push service\n│   │   │   ├── discord/                # Discord bot client, Gateway v10, and subscriber\n│   │   │   └── tgbot/                  # Telegram bot command handlers\n│   │   ├── runtime/            # ⭐⭐ The Local/Remote node abstraction (see §5.2)\n│   │   │   ├── runtime.go      #   the Runtime interface (the contract)\n│   │   │   ├── local.go        #   Local impl → this box's Xray gRPC API\n│   │   │   ├── remote.go       #   Remote impl → HTTPS calls to a child node\n│   │   │   ├── tls_client.go   #   per-node HTTP client: verify / skip / pin / mtls\n│   │   │   └── manager.go      #   RuntimeFor(nodeID) → picks Local or Remote\n│   │   ├── job/               # Cron job structs (one file per job — see §5.4)\n│   │   ├── middleware/        # Gin middleware: security.go (headers/HSTS), bodylimit.go,\n│   │   │                      #   domainValidator.go, validate.go (CSRF), config_envelope.go\n│   │   ├── global/           # Global singletons: web server + sub server handles, restart hook\n│   │   ├── network/          # Custom net listeners (e.g. proxy-protocol aware)\n│   │   ├── session/          # Session/cookie helpers\n│   │   ├── websocket/        # WS hub implementation\n│   │   ├── locale/ + translation/  # i18n middleware + 13 locale JSON catalogs\n│   │   ├── entity/           # Shared request/response DTOs\n│   │   └── dist/             # ⚠️ Vite build output, embedded via go:embed (generated — do not hand-edit)\n│   │\n│   ├── sub/                   # The subscription server (separate from panel)\n│   │   ├── sub.go             #   server bootstrap\n│   │   ├── controller.go      #   routes for raw / JSON / Clash subscription formats\n│   │   ├── service.go         # ⭐ The link/config builder (~2.5k lines — share-link logic lives here)\n│   │   ├── json_service.go    #   JSON subscription format\n│   │   ├── clash_service.go   #   Clash/Mihomo YAML format\n│   │   ├── clash_external.go  #   external Clash config integration\n│   │   ├── external_subscription.go / external_config.go  # external sub import/aggregation\n│   │   ├── host_sub.go        #   Host-row overrides applied to subscription output\n│   │   ├── endpoint.go        #   subscription endpoint configuration\n│   │   ├── vless_route.go     #   VLESS route shaping\n│   │   ├── remark_vars.go     #   remark variable expansion\n│   │   └── links.go           #   link helpers\n│   │\n│   ├── mtproto/              # Embedded MTProto (Telegram) proxy: manager.go + per-OS\n│   │                         #   process supervision + orphan cleanup\n│   ├── logger/              # App logger (op/go-logging + lumberjack rotation)\n│   └── util/                # Leaf helpers (no business logic):\n│       ├── common/          #   errors, misc\n│       ├── crypto/          #   key/cert generation (x25519, ML-KEM/ML-DSA, ECH)\n│       ├── link/            #   outbound share-link building primitives\n│       ├── wirecodec/ + wireguard/  # WireGuard codec + integration helpers\n│       └── random/, json_util/, reflect_util/, sys/, netproxy/, netsafe/, ldap/\n│\n├── frontend/                 # React SPA (built into internal/web/dist)\n│   ├── vite.config.js        # ⭐ Build config: outDir → ../internal/web/dist, dev on :5173\n│   │                         #   (strict) proxying to :2053, entries index/login/subpage.html\n│   ├── package.json          # scripts: dev / build / preview / lint / typecheck / test / gen\n│   └── src/\n│       ├── main.tsx / routes.tsx / queryClient.ts   # SPA entry, router, query client\n│       ├── entries/          # Extra HTML entry points: login.tsx, subpage.tsx\n│       ├── pages/            # ⭐ Route screens. Mirrors the panel's feature areas:\n│       │   ├── inbounds/     #   inbound list + the big inbound form (protocols/security/transport)\n│       │   ├── clients/      #   client management screens\n│       │   ├── nodes/        #   multi-node UI\n│       │   ├── hosts/        #   subscription host-override UI\n│       │   ├── xray/         #   raw Xray config UI (routing, dns, outbounds, balancers, overrides)\n│       │   ├── index/        #   dashboard/home\n│       │   └── settings/, groups/, sub/, login/, api-docs/\n│       ├── api/              # ⭐ Data layer: http-init, QueryProvider, queryKeys, websocket bridge\n│       │   └── queries/      #   TanStack Query hooks (useNodesQuery, useStatusQuery, …)\n│       ├── schemas/          # Zod schemas: protocols, forms, api, primitives\n│       ├── generated/        # ⚠️ GENERATED from Go (see §5.5): schemas.ts, types.ts, zod.ts, examples.ts\n│       ├── components/       # Reusable UI (clients/ form/ geodata/ ui/ viz/ feedback/ utility/)\n│       ├── lib/              # Frontend domain logic (xray/ inbounds/ clients/)\n│       ├── hooks/, models/, layouts/, i18n/, utils/, styles/\n│       └── test/             # Vitest + golden fixtures (config-generation snapshot tests)\n│\n├── tools/openapigen/         # ⭐ Go program that emits frontend/src/generated/* from Go types (§5.5)\n├── docs/                     # Markdown docs (this file, custom-subscription-templates.md, …)\n├── media/                    # README images\n│\n├── Dockerfile / docker-compose.yml / DockerEntrypoint.sh / DockerInit.sh   # Container build/run\n├── install.sh / update.sh / x-ui.sh                        # VPS install + management CLI\n├── x-ui.service.*  / x-ui.rc                               # systemd units (debian/rhel/arch) + rc script\n├── windows_files/                                          # Windows service support\n└── .github/workflows/        # CI: ci.yml, codeql.yml, docker.yml, release.yml,\n                              #     mutation.yml, cleanup_caches.yml, claude-pr-review.yml,\n                              #     claude-issue-analyst.yml\n```\n\n---\n\n## 5. Cross-cutting subsystems (the parts that span many files)\n\n### 5.1 DB → Xray config pipeline (config generation & application)\n\nThe panel never edits Xray's running config directly from controllers. The flow is:\n\n1. A service mutates DB state (inbound/client/setting).\n2. `XrayService` (`service/xray.go`) builds a fresh `xray.Config` from DB state\n   (`GetXrayConfig`).\n3. It tries a **hot apply** (`tryHotApply` → `xray/hot_diff.go`): diff old vs new config and\n   push only the deltas over the Xray gRPC API (add/remove inbound, add/remove user) — **no\n   process restart**, so live connections survive.\n4. If the diff isn't hot-applicable (structural change), it falls back to a **full restart**\n   of the Xray process (`xray/process.go`).\n\nRestart is debounced via an atomic \"need restart\" flag (`SetToNeedRestart` /\n`IsNeedRestartAndSetFalse`), consumed by a `@every 30s` cron task registered in `startTask()`\n— any number of mutations inside the window causes at most one restart.\n\n**Key files:** `service/xray.go` (orchestration), `xray/hot_diff.go` (the diff algorithm),\n`xray/process.go` (process lifecycle), `xray/api.go` (gRPC calls), `xray/config.go` (config model).\n\n### 5.2 Runtime abstraction — Local vs Remote (multi-node) ⭐ most important\n\nA \"node\" (`model.Node`) is another 3x-ui instance this panel controls. Every state-changing\ninbound/client operation goes through the `runtime.Runtime` interface so the _same service\ncode_ works whether the target is the local Xray or a remote node.\n\n- **Interface:** `internal/web/runtime/runtime.go` — `Name`, `AddInbound`, `DelInbound`,\n  `UpdateInbound`, `AddUser`, `RemoveUser`, `UpdateUser`, `DeleteUser`, `AddClient`,\n  `RestartXray`, `ResetClientTraffic`, `ResetInboundTraffic`, `ResetAllTraffics`.\n- **`Local`** (`local.go`): calls this box's Xray gRPC API directly.\n- **`Remote`** (`remote.go`): serializes the operation and sends it over HTTPS to the child\n  node's API.\n- **TLS modes** (`tls_client.go`, per-node `TlsVerifyMode`):\n  `verify` (system CAs, default) / `skip` (no validation) / `pin` (leaf cert SHA-256 must\n  match `PinnedCertSha256`) / `mtls` (master presents a client certificate; node cert checked\n  against system roots; API token optional). Master-side cert management:\n  `service/node_mtls.go` + `service/setting_mtls.go`.\n- **Dispatch:** `manager.go` → `Manager.RuntimeFor(nodeID *int)`; `nil` nodeID → `Local`,\n  otherwise a cached/lazy-loaded `Remote`. `InvalidateNode(id)` drops a cached remote client.\n\nBearer tokens use the narrowest scope that matches the caller's duties:\n\n| Scope        | Access                                                                 |\n| ------------ | ---------------------------------------------------------------------- |\n| `monitor`    | Read-only status, version, and metric endpoints.                       |\n| `node-sync`  | Node configuration, inbound/client synchronization, traffic, and Xray. |\n| `node-admin` | All `node-sync` access plus `POST /panel/api/server/updatePanel`.      |\n| `admin`      | Every panel API endpoint; intended for fully trusted automation.       |\n\n`node-admin` remains a node synchronization identity when inbound rules are applied. This\npreserves the node-specific validation path while keeping software-update authority separate\nfrom ordinary `node-sync` credentials. On an mTLS connection, only a valid `node-admin` bearer\nelevates the request; other bearer scopes retain the historical mTLS `node-sync` behavior.\nExisting tokens retain their stored scope, and legacy tokens without one continue to migrate to\n`admin`.\n\n**Node identity & attribution (the hard part).** Inbounds carry a `NodeID` _and_ an\n`OriginNodeGuid`. Because inbounds can be pushed across hops, the panel attributes traffic and\nonline clients back to the originating panel using **stable GUIDs** rather than local IDs.\nRelevant logic: `service/inbound_node.go` (`ReconcileNode`, `SetRemoteTraffic`, GUID merge,\n`synthNodeGuid`, `panelGuid`) and `service/node.go` (`effectiveNodeGuid`, heartbeat, dirty\ntracking). Node \"dirty\" flags drive an **anti-entropy reconciliation** so an offline node's\ninbound edits converge once it reconnects.\n\n**Where to look for node bugs:**\n\n- Operation not reaching a node → `runtime/remote.go` + `runtime/manager.go`.\n- Wrong traffic/online attribution across hops → `service/inbound_node.go` (GUID merge paths).\n- Node shown offline / stale status → `job/node_heartbeat_job.go` + `service/node.go` (`Probe`, `UpdateHeartbeat`).\n- Edits to an offline node not applying on reconnect → dirty/reconcile logic in `service/inbound_node.go` + `service/node.go` (`MarkNodeDirty`/`ClearNodeDirty`/`NodeSyncState`).\n- TLS/mTLS handshake failures → `runtime/tls_client.go`, `service/node_mtls.go`, `service/node.go` (`FetchCertFingerprint`).\n\n### 5.3 Traffic accounting\n\nPer-client and per-inbound up/down counters originate from Xray's stats API and are persisted\nto the DB. The Xray traffic job polls the core; node traffic is pulled from child nodes and\nmerged with GUID-based baselines to avoid double counting after resets.\n\n**Key files:** `service/inbound_traffic.go`, `service/traffic_writer.go`,\n`job/xray_traffic_job.go`, `job/node_traffic_sync_job.go`, `service/inbound_node.go`\n(`SetRemoteTraffic` / `upsertNodeBaseline`), models `xray.ClientTraffic`,\n`model.NodeClientTraffic`, `model.ClientGlobalTraffic` (cross-master totals).\nA client reset is queued per hosting node in `model.NodePendingReset` (`service/node_reset_queue.go`)\nand replayed by the node sync until the node accepts it.\nPeriodic resets: `job/periodic_traffic_reset_job.go` (keyed off `Inbound.TrafficReset`).\n\n### 5.4 Background jobs (cron)\n\nAll registered in `web.go` → `startTask()`. Each is a struct with a `Run()` method in `internal/web/job/`:\n\n| Schedule            | Job                                                                                              | Purpose / condition                                                                   |\n| ------------------- | ------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------- |\n| `@every 1s`         | `check_xray_running_job`                                                                         | Restart Xray if it died (2 consecutive down checks)                                   |\n| `@every 30s`        | (inline func in `startTask`)                                                                     | Debounced Xray restart — consumes the \"need restart\" flag (§5.1)                      |\n| `@every 5s`         | `xray_traffic_job`                                                                               | Pull traffic stats from Xray (5s start delay)                                         |\n| `@every 5s`         | `node_heartbeat_job`                                                                             | Probe child nodes (online/offline)                                                    |\n| `@every 5s`         | `node_traffic_sync_job`                                                                          | Pull + merge node traffic; push reconciliation                                        |\n| `@every 10s`        | `check_client_ip_job`                                                                            | Enforce per-client IP limits                                                          |\n| `@every 10s`        | `mtproto_job`                                                                                    | Reconcile `mtg` sidecars against enabled MTProto inbounds                             |\n| `@every 10s`        | `amneziawg_job`                                                                                  | Reconcile embedded AmneziaWG interfaces against enabled local inbounds                |\n| `@every 5m`         | `outbound_subscription_job`                                                                      | Refresh outbound provider configs                                                     |\n| `@every 10m`        | `clear_logs_job` (`PruneXrayLogsJob`)                                                            | Truncate Xray access/error logs once either exceeds 64 MiB                            |\n| `@hourly`           | `warp_ip_job`, `periodic_traffic_reset_job(\"hourly\")`                                            | WARP IP rotation; traffic resets                                                      |\n| `@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       |\n| `@weekly`           | `periodic_traffic_reset_job(\"weekly\")`                                                           | Weekly traffic resets                                                                 |\n| default `@every 1m` | `ldap_sync_job`                                                                                  | Only if LDAP enabled; schedule configurable                                           |\n| default `@daily`    | `stats_notify_job`                                                                               | Only if TG bot enabled; schedule configurable                                         |\n| default `@daily`    | `discord_notify_job`                                                                             | Only if Discord bot enabled; schedule configurable                                    |\n| `@every 2m`         | `check_hash_storage`                                                                             | Only if TG bot enabled; expires bot callback hashes                                   |\n| `@every 1m`         | `check_cpu_usage`                                                                                | Only if a CPU alarm is configured (TG, Discord, or email); publishes `cpu.high`       |\n| `@every 1m`         | `check_memory_usage`                                                                             | Only if a memory alarm is configured (TG, Discord, or email); publishes `memory.high` |\n| configurable        | `free_os_memory`                                                                                 | Only if `sys.MemoryReleaseIntervalMinutes() > 0`; returns heap to OS                  |\n\nTo change _when_ something runs, edit `startTask()`. To change _what_ it does, edit the job file.\n\n### 5.5 Type generation (Go → TypeScript) ⚠️ don't hand-edit generated files\n\nThe Go backend is the schema source of truth. `tools/openapigen` (a Go program, with a\n`StructAllow` allowlist of exported types) emits\n`frontend/src/generated/{schemas,types,zod,examples}.ts`. The frontend build runs this first:\n\n- `npm run gen:zod` → `go run ./tools/openapigen` (regenerate from Go)\n- `npm run gen:api` → builds the OpenAPI doc (`scripts/build-openapi.mjs`, driven by the\n  hand-maintained endpoint registry `src/pages/api-docs/endpoints.ts`)\n- `npm run build` runs `gen:api` then `vite build`.\n\n**Implication:** if you change a Go model/DTO that crosses the API boundary, regenerate the\nfrontend types (`cd frontend && npm run gen`) instead of editing `src/generated/` by hand.\n\n### 5.6 Share-link / subscription generation\n\nTwo distinct code paths produce client configs:\n\n- **Per-client links in the panel** (the \"copy link\" / QR in the UI): `service/client_link.go`\n  - `util/link/outbound.go`.\n- **Subscription endpoint** (what a client app polls): `internal/sub/service.go` (raw links),\n  `internal/sub/json_service.go` (JSON), `internal/sub/clash_service.go` (Clash YAML).\n  **`Host` rows** (`model.Host`, edited under /panel/api/hosts) override address/SNI/path/\n  security per inbound in subscription output — applied in `sub/host_sub.go`.\n\nBoth paths must agree per protocol. A malformed link for a specific protocol/transport combo\n(e.g. XHTTP + Reality) is usually a field-lookup mismatch in **`internal/sub/service.go`** (and\nits tests `service_test.go` / golden fixtures), or in `util/link/outbound.go`. The frontend\nalso has protocol schemas under `frontend/src/schemas/protocols/` and `frontend/src/lib/xray/`.\n\n### 5.7 Event bus (in-process pub/sub)\n\n`internal/eventbus/` is a minimal buffered-channel pub/sub. Producers call a non-blocking\n`Publish(Event)`; all subscribers receive every event. Event types: `outbound.down|up`,\n`xray.crash`, `node.down|up`, `cpu.high`, `memory.high`, `login.attempt`, with structured\npayloads (OutboundHealthData, NodeHealthData, LoginEventData, SystemMetricData). Producers\ninclude the CPU/memory jobs, node heartbeat, and login handling; consumers include the\nTelegram bot, the Discord bot (`service/discord/`), and the email notifier (`service/email/`). Use it for cross-cutting\nnotifications instead of importing notification services into producers.\n\n### 5.8 Tunnel health monitor\n\n`internal/tunnelmonitor/` is an optional watchdog configured **only via env vars**\n(`XUI_TUNNEL_HEALTH_*`, read in `internal/config/`), deliberately independent of panel\nsettings so it can be enabled from a systemd `EnvironmentFile` even when the panel is\nunreachable. It periodically probes an HTTP URL (default: Cloudflare trace endpoint) through\nthe tunnel; after N successive failures (default 3) it fires a recovery callback wired to an\nXray restart.\n\n---\n\n## 6. Data model cheat-sheet\n\nGORM models in `internal/database/model/` (main file `model.go` + siblings); all registered\nfor AutoMigrate in `internal/database/db.go`.\n\n| Model                           | Table role                                | Notable fields                                                                                                                                                     |\n| ------------------------------- | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| `User`                          | Admin login                               | bcrypt password, `LoginEpoch` (invalidates sessions)                                                                                                               |\n| `Inbound`                       | An Xray inbound                           | `Tag` (unique), `Port`, `Protocol`, `Settings`/`StreamSettings`/`Sniffing` (JSON), `Enable`, `TrafficReset`, `NodeID`, **`OriginNodeGuid`**, `ClientStats` (assoc) |\n| `Client`                        | In-memory client view                     | UUID/email/flow/limits (parsed from inbound JSON; not persisted)                                                                                                   |\n| `ClientRecord`                  | Persisted client (`clients`)              | `Email` (unique), `SubID`, `UUID`, `TotalGB`, `ExpiryTime`, `LimitIP`, `Group`, `Reset`                                                                            |\n| `ClientGroup` / `ClientInbound` | Grouping + client↔inbound join            | many-to-many wiring, `FlowOverride`                                                                                                                                |\n| `ClientExternalLink`            | Extra links attached to a client          | `Kind`, `Value`, `Remark`, `SortIndex`                                                                                                                             |\n| `Host`                          | Subscription host overrides (per inbound) | `Address`, `Port`, `Sni`, `Path`, `Security`, `Fingerprint`, `SortOrder`, visibility/exclusion flags                                                               |\n| `Node`                          | A managed child panel                     | `Guid`, `Address`, `Status`, `TlsVerifyMode`, `PinnedCertSha256`, `ConfigDirty`, version/heartbeat/metric fields                                                   |\n| `NodeClientTraffic`             | Per-node client traffic baseline          | cross-node merge (anti-double-count)                                                                                                                               |\n| `NodePendingReset`              | Client resets a node has not confirmed    | `NodeId`, `Email`, `QueuedAt`; replayed by the node sync, freezes that client's node verdict until delivered                                                       |\n| `NodeClientIp`                  | Per-node client IP attribution            | `NodeGuid`, `Email`, `Ips`                                                                                                                                         |\n| `ClientGlobalTraffic`           | Cross-master usage totals                 | `MasterGuid`, `Email`, `Up`, `Down`                                                                                                                                |\n| `xray.ClientTraffic`            | Per-client counters (`client_traffics`)   | `Email`, `Up`, `Down`, `Total`, `ExpiryTime`, `LastOnline`                                                                                                         |\n| `InboundClientIps`              | IP set per client email                   | drives IP-limit enforcement                                                                                                                                        |\n| `OutboundTraffics`              | Outbound counters                         | per outbound tag                                                                                                                                                   |\n| `OutboundSubscription`          | External provider subs                    | Warp/Nord style                                                                                                                                                    |\n| `Setting`                       | Key/value panel settings                  | everything configurable                                                                                                                                            |\n| `ApiToken`                      | REST API tokens                           | SHA-256 hash (plaintext shown once)                                                                                                                                |\n| `InboundFallback`               | Fallback routing on a shared port         | SNI/ALPN/path → dest                                                                                                                                               |\n| `HistoryOfSeeders`              | Seeder bookkeeping                        | prevents re-running one-off migrations                                                                                                                             |\n\n---\n\n## 7. Symptom → File index (start here when debugging)\n\n| Symptom / task                                                                    | Primary file(s)                                                              | Then check                                                                                          |\n| --------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- |\n| 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`                            |\n| **Inbound** create/update/delete behavior                                         | `service/inbound.go`, `service/inbound_clients.go`                           | `runtime/*`, `service/xray.go`                                                                      |\n| **Client** CRUD / limits / expiry                                                 | `service/client_crud.go`, `service/client_inbound_apply.go`                  | model `ClientRecord`, `service/inbound_traffic.go`                                                  |\n| **Bulk** client operations slow/wrong                                             | `service/client_bulk.go`                                                     | `service/client_paging.go`                                                                          |\n| Xray **won't apply** a config change                                              | `service/xray.go` (`RestartXray`, `tryHotApply`)                             | `xray/hot_diff.go`, `xray/process.go`                                                               |\n| Xray **restarts when it shouldn't** (kills connections)                           | `xray/hot_diff.go` (diff not classified as hot)                              | `service/xray.go`                                                                                   |\n| **Traffic** counts wrong / reset behavior                                         | `service/inbound_traffic.go`, `job/xray_traffic_job.go`                      | `service/traffic_writer.go`, `job/periodic_traffic_reset_job.go`                                    |\n| **Node** operation not propagating                                                | `runtime/remote.go`, `runtime/manager.go`                                    | `service/inbound_node.go`                                                                           |\n| **Multi-hop / cross-node attribution** (traffic or online clients on wrong panel) | `service/inbound_node.go` (GUID merge, `synthNodeGuid`, `effectiveNodeGuid`) | `service/node.go`, model `OriginNodeGuid`/`Node.Guid`                                               |\n| Node stuck **offline / stale**                                                    | `job/node_heartbeat_job.go`, `service/node.go` (`Probe`, `UpdateHeartbeat`)  | `runtime/tls_client.go` (TLS verify)                                                                |\n| Node **TLS / mTLS** auth failures                                                 | `runtime/tls_client.go`, `service/node_mtls.go`, `service/setting_mtls.go`   | `service/node.go` (`FetchCertFingerprint`)                                                          |\n| Offline node edits **not reconciling** on reconnect                               | `service/inbound_node.go` (`ReconcileNode`, dirty flags)                     | `service/node.go` (`MarkNodeDirty`/`NodeSyncState`)                                                 |\n| **Share link / QR** malformed (per protocol)                                      | `service/client_link.go`, `util/link/outbound.go`                            | `frontend/src/lib/xray/`, `frontend/src/schemas/protocols/`                                         |\n| **Subscription** output wrong (raw/JSON/Clash)                                    | `internal/sub/service.go`                                                    | `sub/json_service.go`, `sub/clash_service.go`, sub golden tests                                     |\n| Subscription **host overrides** not applied                                       | `service/host.go`, `sub/host_sub.go`                                         | model `Host`, `frontend/src/pages/hosts/`                                                           |\n| **External subscription** import/aggregation                                      | `sub/external_subscription.go`, `sub/external_config.go`                     | `sub/clash_external.go`                                                                             |\n| **Settings** not saving / defaults                                                | `service/setting.go`, `controller/setting.go`                                | model `Setting`                                                                                     |\n| **Login / 2FA / sessions / CSRF**                                                 | `controller/index.go`, `service/panel/user.go`, `middleware/`                | `session/`                                                                                          |\n| **API tokens**                                                                    | `service/panel/api_token.go`, `controller/setting.go`                        | model `ApiToken`                                                                                    |\n| **Port conflict** on inbound add                                                  | `service/port_conflict.go`                                                   | `controller/inbound.go`                                                                             |\n| **Fallbacks** (shared 443, SNI routing)                                           | `service/fallback.go`, `controller/inbound.go`                               | model `InboundFallback`                                                                             |\n| **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()` |\n| **`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/`                            |\n| **Telegram bot** commands                                                         | `service/tgbot/`                                                             | `job/stats_notify_job.go`                                                                           |\n| **Discord bot** commands & reports                                                | `service/discord/`                                                           | `job/discord_notify_job.go`                                                                         |\n| **Email notifications**                                                           | `service/email/`                                                             | `internal/eventbus/` (consumers)                                                                    |\n| **CPU / memory alerts** not firing                                                | `job/check_cpu_usage.go`, `job/check_memory_usage.go`                        | `internal/eventbus/`, notifier settings in `service/setting.go`                                     |\n| Xray auto-restart on **dead tunnel**                                              | `internal/tunnelmonitor/`                                                    | `XUI_TUNNEL_HEALTH_*` in `internal/config/`                                                         |\n| **WARP / Nord / PIA** outbound integration                                        | `service/integration/warp.go` / `nord.go` / `pia.go`                         | `internal/pia/`, `frontend/src/pages/xray/overrides/`                                               |\n| **MTProto** proxy issues                                                          | `internal/mtproto/manager.go`, `mtproto/process*.go`                         | `job/mtproto_job.go`                                                                                |\n| **DB migration** / new column                                                     | `internal/database/db.go` (AutoMigrate list), `migrate_data.go`              | `model/model.go`                                                                                    |\n| **Cron schedule** changes                                                         | `web.go` → `startTask()`                                                     | the specific `job/*.go`                                                                             |\n| **CORS / security headers / HTTPS**                                               | `middleware/`, `web.go` (`initRouter`, TLS setup)                            | `config/` (env)                                                                                     |\n| **Env vars / paths / DB type**                                                    | `internal/config/config.go`                                                  | `.env.example`                                                                                      |\n| **Frontend route / screen**                                                       | `frontend/src/pages/<area>/`, `frontend/src/routes.tsx`                      | `frontend/src/api/queries/`                                                                         |\n| **Frontend ↔ backend type mismatch**                                              | regenerate: `cd frontend && npm run gen` (`tools/openapigen`)                | `frontend/src/generated/`                                                                           |\n| **System status / CPU / metrics**                                                 | `service/server.go`, `service/xray_metrics.go`, `service/metric_history.go`  | `controller/server.go`, gopsutil                                                                    |\n\n---\n\n## 8. Layering rules (where new code belongs)\n\n1. **Controllers are thin.** Only: bind/validate input, call one service, shape the HTTP\n   response. No DB queries, no Xray calls, no business rules in `controller/`.\n2. **Services own the logic and transactions.** All business rules, DB access, and decisions\n   about applying changes live in `service/`. If you're tempted to query GORM from a\n   controller, move it to a service.\n3. **Never touch Xray's running state from a controller or job directly.** Go through\n   `XrayService` / the `runtime.Runtime` interface so local vs node dispatch stays correct.\n4. **Any state-changing inbound/client o","isInternal":false,"tokens":10394,"sizeBytes":51742},{"name":"CLAUDE.md","path":"CLAUDE.md","rawUrl":"https://raw.githubusercontent.com/MHSanaei/3x-ui/HEAD/CLAUDE.md","title":"Claude Agent Guidelines & System Prompt","category":"claude-rule","format":"markdown","content":"# CLAUDE.md\n\nOperational guide for AI agents working in this repo. Long-form human docs:\n`CONTRIBUTING.md` (setup, testing philosophy) and `frontend/README.md`.\nRead those before large changes. This file is the short, must-follow version.\nFor a deep navigation map (request lifecycle, cron-job table, symptom → file\nindex, layering rules), read `docs/architecture.md` on demand — do not guess\nfile locations when it can answer in one hop.\n\n## Stack\n- Backend: Go 1.27 (`module github.com/mhsanaei/3x-ui/v3`), Gin, GORM.\n  Runs Xray-core as a managed child process (`internal/xray/process.go`) and\n  imports `github.com/xtls/xray-core` for config types + gRPC stats/handler/router\n  API. MTProto inbounds run a second managed child — the `mtg-multi` binary\n  (a multi-secret mtg fork — NOT a Go dependency; its prebuilt release binary is\n  fetched at image/release build time by `DockerInit.sh` + `release.yml`,\n  panel-side code in `internal/mtproto/`) — outside Xray, one process per inbound\n  serving each\n  client's FakeTLS secret via the fork's `[secrets]` section (plus per-client\n  ad-tags via `[secret-ad-tags]` and per-client data quota / expiry via\n  `[secret-limits]`, mapped from the client's `totalGB`/`expiryTime`). Client,\n  ad-tag and quota/expiry edits are hot-applied through the fork's management API\n  (`PUT /secrets`, bearer-token guarded) so connections survive; the manager\n  falls back to a process restart on older binaries. A client's panel-side\n  traffic reset also calls `POST /secrets/{name}/reset-quota` so a renewed client\n  is not re-blocked by the sidecar's quota counter.\n- Storage: SQLite by default (`/etc/x-ui/x-ui.db` on Linux; the executable dir on\n  Windows), PostgreSQL optional (`XUI_DB_TYPE` / `XUI_DB_DSN`). The CGo SQLite\n  driver (`mattn/go-sqlite3`) needs a C compiler — `CGO_ENABLED=0` builds fail.\n- Frontend: React 19 + Ant Design 6 + Vite 8 + TypeScript in `frontend/`,\n  built into `internal/web/dist/` (gitignored) and embedded via `embed.FS`.\n\n## Repo map\n- `main.go` — entry point + `x-ui` CLI (run, migrate, migrate-db, encrypt-tokens, setting, cert).\n- `internal/config/` — env parsing (XUI_DEBUG, XUI_LOG_LEVEL, XUI_LOG_FOLDER,\n  XUI_BIN_FOLDER, XUI_SKIP_HSTS, XUI_PORT, XUI_DB_*).\n- `internal/database/` + `internal/database/model/` — GORM schema (~24 models;\n  Inbound, Client, Setting, User are the core), inbound Protocol enum,\n  AutoMigrate + hand-written migrations in `db.go`.\n- `internal/xray/` — Xray child-process lifecycle, config generation, gRPC API.\n- `internal/xray/geodata/` — streaming geosite/geoip `.dat` reader (cached\n  category index + paged entries) and `geosite:`/`geoip:`/`ext:` token parsing.\n- `internal/mtproto/` — MTProto inbounds via the bundled `mtg-multi` binary.\n- `internal/tuic/` — TUIC v5 inbounds: `tuic-server` sidecar supervisor, native Go UDP relay traffic metering.\n- `internal/amneziawg/` — AmneziaWG protocol shape: instance/peer derivation\n  from an inbound, 3.1 obfuscation param generation + validation, port-forward\n  spec parsing.\n- `internal/amneziawgnet/` — embedded AmneziaWG runtime: amneziawg-go device\n  over a gVisor userspace netstack, per-inbound reconcile manager, TCP/UDP\n  relay into a loopback per-peer-auth SOCKS5 Xray inbound, port-forward\n  listeners, per-peer IPv6 egress aliases.\n- `internal/pia/` — PIA WireGuard protocol client (auth, signed server list, `/addKey`).\n- `internal/sub/` — subscription server (raw / JSON / Clash).\n- `internal/eventbus/` — in-process pub/sub (outbound/node health, xray.crash,\n  cpu.high, memory.high, login.attempt).\n- `internal/logger/`, `internal/util/` (link, crypto, sys, ldap, …),\n  `internal/tunnelmonitor/` — shared infrastructure.\n- `internal/web/` — Gin server (embeds `dist/` + `translation/`).\n  - `controller/` — panel + REST API handlers; OpenAPI at /panel/api/openapi.json.\n  - `service/` — business logic (InboundService, SettingService, XrayService,\n    node sync); subpackages tgbot/, discord/, email/, outbound/, panel/, integration/.\n  - `job/` — 19 cron jobs (traffic, fail2ban IP-limit, node heartbeat/sync, LDAP,\n    CPU/memory watchdogs, …); full table in `docs/architecture.md` §5.4.\n  - `middleware/`, `entity/`, `global/`, `session/` (CSRF), `network/`,\n    `runtime/` (master/sub-node over mTLS), `websocket/`.\n  - `locale/` + `translation/` — i18n, 13 embedded locale JSON files.\n- `frontend/` — React + TS source (see `frontend/CLAUDE.md`).\n- `tools/openapigen/` — Go generator that emits frontend types + Zod/JSON schemas\n  into `frontend/src/generated/` from Go structs. The OpenAPI doc itself\n  (`frontend/public/openapi.json`) is assembled from those + `endpoints.ts` by\n  `frontend/scripts/build-openapi.mjs`.\n- `docs/` — separate Next.js/Fumadocs site (pnpm, own CI in `docs-ci.yml`,\n  outside `make verify`). Holds a THIRD independent implementation of\n  link/subscription generation in `docs/lib/xray/` — check it whenever\n  share-link or install-command output changes.\n\n## Hard rules (non-negotiable)\n- Correct fix over small fix. Find the root cause and fix it the right way, however\n  much code that takes. Size the change by what the correct fix needs, never by\n  line count: when the right fix spans many files, or needs a migration, a shared\n  helper or a new abstraction, write it. A guard that hides the symptom while the\n  cause survives is the wrong fix, however small. Two limits remain:\n  - Everything added must be something the correct fix needs. No speculative\n    knobs, unused extension points or \"while I was here\" rewrites. Unrelated\n    refactors and cleanups go in their own commit.\n  - Stop and ask only when the right fix needs a decision the code cannot answer:\n    a deliberate user-visible behaviour change, or two sound designs with a real\n    trade-off. Ask with a recommendation. Size alone is never a reason to stop,\n    defer or ship a smaller patch.\n- Comments in committed Go/TS: 2 lines MAX per comment block. Make the name\n  carry the meaning first and rename rather than annotate; spend the 2 lines on\n  the *why* a name cannot hold — an invariant, an issue number, a non-obvious\n  constraint. Exempt: `//go:build`, `//go:generate`, and other directives.\n  HTML `<!-- -->` is fine. (A linter cannot enforce this — you must.)\n- New `g.POST`/`g.GET` in `internal/web/controller/` REQUIRES a matching entry\n  in `frontend/src/pages/api-docs/endpoints.ts`, then `make gen` (or\n  `cd frontend && npm run gen`). Hand-maintained but pinned both ways by\n  `TestRouteRegistryContract` (`internal/web/routes_contract_test.go`): a missing\n  OR stale entry fails `make test-go`. Scope: `/panel/api/*` + a few session\n  routes; sub-server routes are exempt.\n- Response examples come from Go struct `example:` tags via `tools/openapigen` —\n  never hand-write them. A new struct must be added to openapigen's `StructAllow`\n  allowlist (`tools/openapigen/main.go`) or it is silently omitted from\n  schemas/examples (and `build-openapi.mjs` then fails on the missing schema).\n- A new or renamed endpoint has a FOURTH step nothing checks: copy\n  `frontend/public/openapi.json` → `docs/public/openapi.json`, then\n  `cd docs && pnpm gen:api` to refresh the MDX under\n  `docs/content/docs/en/reference/api/`. `docs-ci.yml` fires only on `docs/**`.\n- A new English i18n key goes in EVERY locale JSON in `internal/web/translation/`\n  (13 files) AND must be referenced from `frontend/src` or Go in the SAME commit —\n  `frontend/src/test/i18n-dead-keys.test.ts` fails both ways. It is a frontend\n  test, so run `npm test`, not just `make test-go`. At runtime the frontend falls\n  back to en-US; Go (`internal/web/locale/`) returns \"\" for an unknown key.\n- DB / model changes require a migration in `internal/database/db.go`.\n- Every state-changing inbound/client op dispatches through `runtime.Runtime`\n  (`internal/web/runtime/`) — never straight to `internal/xray/api.go`, never from\n  a controller or cron job. A direct call passes every local test and silently\n  breaks every multi-node deployment. Other layering rules: `docs/architecture.md` §8.\n- Conventional commits: `type(area): short imperative summary`, then a body\n  explaining the why. Types in use: `fix`, `feat`, `chore`, `refactor`, `perf`,\n  `docs`, `style`.\n\n## Tests: TDD, and only tests that can fail (Go and frontend)\n- Work red → green → refactor.\n  - Bug: turn the reproduction into a test first, and watch it fail for the\n    reported reason.\n  - Feature: write the test for the first behaviour before writing its code.\n  - Then write the code that makes it pass, and refactor with the suite green.\n\n  If a test was written after the code, prove it anyway: revert the code, watch\n  the test go red, then restore. A test that passes either way is worse than no\n  test. It certifies nothing, and then gets cited as proof the fix works.\n- Every test must name the failure it catches. When no test can reach a change\n  (workflow YAML, pure wiring, layout), say so and name the command that\n  demonstrates it. Never write a stand-in test.\n- Fake tests are forbidden. Delete any you write or meet in the code you touch:\n  - tests of a getter, a constant, a rename, a pure map lookup, or an input the\n    function can never receive;\n  - tests that restate the implementation, such as recomputing the expected\n    value with the same formula or asserting that a mock was called exactly the\n    way the code calls it;\n  - mocking the unit under test, or mocking so much around it that the real\n    code path never runs;\n  - assertions too weak to fail: `err != nil`, `len > 0`, `toBeDefined()`, or\n    `not.toThrow()` alone;\n  - golden files or snapshots regenerated to match whatever the code now outputs;\n  - extra cases that exercise no distinct branch, and tests written to raise\n    coverage.\n\n  One real test that drives the bug through the actual code path beats five\n  that restate the code.\n\n## Go conventions\n- Stdlib `testing` only (no testify). Table-driven, `t.Run` subtests,\n  `t.Helper()` on helpers. Assert the exact value / typed error / emitted\n  string, never just `err != nil`. Prefer real deps over mocks: throwaway DB via\n  `dbtest.InitDB(t, filepath.Join(t.TempDir(), \"x-ui.db\"))`\n  (`internal/database/dbtest`: copies a once-migrated template and registers\n  `CloseDB` cleanup; a fresh `database.InitDB` costs ~7x more, ~850ms under\n  `-race`); `httptest` for HTTP. Keep `database.InitDB` for reopening a file or\n  migrating a hand-built legacy DB. `internal/sub`'s `initSubDB(t)` is the template.\n- Code must pass `golangci-lint run` (gofumpt + goimports formatting): `make lint`.\n- Postgres, xray-gRPC-e2e, master+node and scale tests `t.Skip` unless\n  `XUI_TEST_PG_DSN`, `XUI_DB_TYPE`+`XUI_DB_DSN`, `XRAY_E2E_BINARY`,\n  `XUI_NODE_E2E_BINARY` or `XUI_SCALE_TEST` is set — a green `go test ./...`\n  does not mean those paths ran.\n\n## Frontend conventions (summary; full version in frontend/CLAUDE.md)\n- Ant Design 6 only — no Tailwind/shadcn. Targeted tweaks, not rewrites.\n- TS strict; `@typescript-eslint/no-explicit-any` is an error. Zod schemas in\n  `src/schemas/` are the source of truth; infer types with `z.infer`, never\n  hand-write. Do not edit `src/generated/`.\n- Node 26 (`.nvmrc`) — `make gen` imports `.ts` directly and needs its type\n  stripping; Node 22 dies with `ERR_UNKNOWN_FILE_EXTENSION`. `npm test` includes\n  a headless-Chromium Storybook project, so run\n  `npx playwright install --with-deps chromium` once or `make verify` fails.\n- Editing `frontend/src` does NOT change what users see until the Vite build is\n  regenerated into `internal/web/dist/`. In `XUI_DEBUG=true`, HTML is served from\n  the frozen embedded FS but JS/CSS off disk — after `npm run build` you MUST\n  restart `go run .` or you get a blank page with 404s.\n- After touching share-link logic (`src/lib/xray/`), run `npm run test` (golden\n  fixtures); regenerate snapshots (`npx vitest run -u`) only for intentional\n  output changes, never to make a red test green.\n\n## Build, test, verify\nA fresh clone has no `internal/web/dist/`, so a bare `go build ./...` dies with\n`pattern all:dist: no matching files found` while ~35 other packages pass — it\nreads as a broken repo, not a missing step. Run `make dist-stub` once; every\n`make` Go target already depends on it, which is why `make test-go` beats\n`go test ./...`. Run `make help` for all targets. The local gate:\n\n    make verify   # gen-check + lint + typecheck + test + build + build-storybook\n\nThat is the *fast* gate, not all of CI. `ci.yml` also runs `make race`,\n`make vulncheck`, a live-Postgres job (where a SKIP counts as a failure),\n`make node-e2e` (a real master and node panel, `internal/nodee2e/`) and a\n30s fuzz smoke on `FuzzParseLink`/`FuzzDecodeCertPin` — run those locally when\nyou touch DB/dialect, node sync or parser code. Node sync has two layers: every\n`runtime.Remote` call gets a cell in `internal/web/node_contract_test.go` (fast,\nin `make test-go`; a method without one fails it), and a multi-tick flow (cron,\nadopt, node down) gets one in `internal/nodee2e/node_sync_test.go`.\n\nCommon targets: `make gen` (regenerate Zod/OpenAPI), `make lint` (Go + frontend),\n`make test` (Go `-shuffle=on` + frontend), `make race`, `make build`. See `Makefile`.\n\n## Definition of done (before opening a PR)\n1. `make verify` passes — its `gen-check` already runs `make gen` and fails on a\n   dirty `frontend/src/generated` / `frontend/public/openapi.json`.\n2. Diff is focused; refactors are separate from feature work.\n","isInternal":false,"tokens":3611,"sizeBytes":13563},{"name":"CLAUDE.md","path":"frontend/CLAUDE.md","rawUrl":"https://raw.githubusercontent.com/MHSanaei/3x-ui/HEAD/frontend/CLAUDE.md","title":"frontend - Claude Agent Guidelines & System Prompt","category":"claude-rule","format":"markdown","content":"# frontend/CLAUDE.md\n\nFrontend agent guide. Full detail: `frontend/README.md` and the root\n`CONTRIBUTING.md` (\"Working on the frontend\"). This is the short version.\n\n## What this is\nReact 19 + Ant Design 6 + Vite 8 + TypeScript. The Vite config is\n`vite.config.js` (plain JS). Three bundles, each emitted into\n`internal/web/dist/` and embedded into the Go binary:\n- `index.html` — admin panel SPA (entry `src/main.tsx`; react-router under\n  `/panel`, lazy routes).\n- `login.html` — login + 2FA (`src/entries/login.tsx`).\n- `subpage.html` — public subscription viewer (`src/entries/subpage.tsx`).\nThe `@` import alias maps to `src/`.\n\n## Data flow\n- Server state via TanStack Query (`src/api/`, keys in `src/api/queryKeys.ts`);\n  invalidate on mutation. WebSocket pushes feed the cache\n  (`src/api/websocketBridge.ts`).\n- Local UI state in the page (`useState`); shared concerns via `src/hooks/`.\n  Extend an existing hook before adding a global.\n- Zod (`src/schemas/`) is the single source of truth for the xray config model.\n  Infer types with `z.infer`. Go-side types are mirrored into `src/generated/`\n  by `npm run gen:zod` (`go run ./tools/openapigen`) — do not hand-edit that\n  folder (every file is marked `DO NOT EDIT`).\n- xray domain logic (links, defaults, form<->wire adapters) is pure functions in\n  `src/lib/xray/`. HTTP goes through `HttpUtil` in `src/utils/index.ts`.\n\n## Rules\n- Ant Design 6 for components; no Tailwind/shadcn (a migration was rolled back).\n  Form *state* runs on React Hook Form (`src/components/form/rhf/`), not Ant\n  Design's `Form` store.\n- Function components + hooks only; no class components.\n- Comments in committed TS/TSX: 2 lines MAX per comment block, spent on the\n  *why* a name cannot hold (same rule as root CLAUDE.md). HTML comments are fine.\n- TS strict; oxlint's `typescript/no-explicit-any` is an error. Build forms with `useZodForm` +\n  `FormField` from `@/components/form/rhf` (wrap the tree in `FormProvider`);\n  validate through the `zodResolver` or per-field\n  `rules={{ validate: rhfZodValidate(Schema.shape.field) }}` — messages are Zod\n  issue keys resolved via `t()`, never inline `z.string()`. AntD `<Form>` stays\n  only as a layout wrapper. Complex shared config editors (FinalMask / Sniffing /\n  Sockopt) remain AntD-`Form` islands wrapped as value/onChange adapters in\n  `src/lib/xray/forms/fields/`, bound via a `Controller`.\n- New `g.POST`/`g.GET` route => add it to `src/pages/api-docs/endpoints.ts`,\n  then `npm run gen`.\n- i18n strings live in `internal/web/translation/<locale>.json`, NOT under\n  `frontend/`, and are shared with the Go backend. A new English key must be\n  added to every locale. Interpolation here uses single braces `{var}`, not the\n  i18next default `{{var}}`.\n- Persian/Arabic (RTL) users are first-class — isolate code identifiers on their\n  own line when writing Persian text in labels/toasts.\n- Vite is pinned to an exact version (no `^`) — bump deliberately, then verify\n  `npm run dev` AND `npm run build`.\n\n## Adding a panel route\n1. `src/pages/<page>/<Page>.tsx` (kebab folder, PascalCase component).\n2. Register in `src/routes.tsx` under `/panel` (lazy import).\n3. Add a sidebar link in `src/layouts/AppSidebar.tsx` if it needs nav.\nOnly standalone bundles (login/subpage) need a new `.html` + `src/entries/*` +\n`rollupOptions.input` (in `vite.config.js`) + a Go controller route.\n\n## Commands\n- `npm run dev` (HMR on :5173, proxies to the Go panel on :2053 — start Go first).\n- `npm run typecheck` / `npm run lint` / `npm run test` / `npm run build`.\n- `npm run gen` = `gen:zod` (Go → `src/generated/`) + `gen:api`\n  (`build-openapi.mjs` → `public/openapi.json`).\n- `npm run storybook` (workbench on :6006) / `npm run build-storybook` (CI\n  compile-checks every story). Reusable `src/components/` get a co-located\n  `<Component>.stories.tsx` with `tags: ['autodocs']`; document props via\n  `argTypes` / `parameters.docs` string metadata, never JSDoc.\n- After `npm run build`, RESTART `go run .` (see the XUI_DEBUG gotcha in root\n  CLAUDE.md) before checking the panel.\n","isInternal":false,"tokens":1150,"sizeBytes":4089}],"systemPromptSnippet":"<yakaai_skills repo=\"MHSanaei/3x-ui\">\n<!-- File: docs/architecture.md (Tokens: ~10394 | Category: project-spec) -->\n# 3x-ui — Architecture & Code Map\n\n> Navigation map for contributors and AI coding agents (referenced from `CLAUDE.md`).\n> Goal: jump to the right file in one hop instead of grepping the whole tree.\n> Tracks the `main` branch — paths reflect the latest changes, so verify against the live\n> tree rather than a pinned release (Go module `github.com/mhsanaei/3x-ui/v3`).\n>\n> **How to use this file:** read \"Mental model\" + \"Request lifecycle\" first, then\n> use the **Symptom → File index** to locate work. Respect the **Layering rules**\n> when adding code. Verify with the commands in **Build / Test / Lint**.\n\n---\n\n## 1. Mental model (the 30-second version)\n\n3x-ui is a **web control panel for [Xray-core](https://github.com/XTLS/Xray-core)**. The Go\nbackend is the source of truth: it stores inbounds/clients/settings in a DB, renders an\nXray JSON config from that state, supervises the Xray child process, and exposes a REST +\nWebSocket API. A React SPA (built by Vite, embedded into the Go binary) is the UI. A second,\nseparate HTTP server serves **subscription links** to end users.\n\nThe panel supervises **managed child processes**: Xray-core itself and — when MTProto\ninbounds exist — a dedicated child proxy binary:\n\n- **`mtg-multi` for MTProto inbounds** (`github.com/mhsanaei/mtg-multi`, a multi-secret fork\n  built from source; `internal/mtproto/`): One process per inbound serves every attached\n  client's FakeTLS secret through the fork's `[secrets]` section, plus optional per-client\n  sponsored-channel ad-tags via `[secret-ad-tags]`. A client or ad-tag edit is hot-applied via\n  the fork's management API (`PUT /secrets`, guarded by a per-process bearer token), with a\n  process restart as the fallback on older binaries.\n\nIn contrast, **AmneziaWG** (`internal/amneziawgnet/`) and **TUIC v5** (`internal/tuic/`) run as\n**in-process native Go servers** without external child processes, bridging client traffic into\nXray-core via loopback SOCKS5 relays.\n\nServers and processes, all launched from `main.go`:\n\n| Server / process | Package                           | Purpose                                                            | Default port      |\n| ---------------- | --------------------------------- | ------------------------------------------------------------------ | ----------------- |\n| **Panel**        | `internal/web`                    | Admin REST/WS API + serves the embedded SPA                        | 2053              |\n| **Subscription** | `internal/sub`                    | Public endpoint that hands out client configs (raw / JSON / Clash) | `subPort` setting |\n| **Xray-core**    | supervised via `internal/xray`    | The actual proxy engine; a child process, not Go code              | `inbounds[].port` |\n| **mtg-multi**    | supervised via `internal/mtproto` | MTProto proxy child process for MTProto inbounds (multi-secret)    | per inbound       |\n\nTwo key ideas that explain most of the complexity:\n\n1. **The DB → Xray config pipeline.** Inbounds/clients live in the DB. On every change the\n   backend regenerates the Xray config and applies it — preferring a _hot diff_ (live gRPC\n   API mutation) over a full process restart. See §5.1.\n2. **The Runtime abstraction (multi-node).** A panel can manage remote \"nodes\" (other 3x-ui\n   instances). Every state-changing inbound/client operation is dispatched through a\n   `runtime.Runtime` interface that is either **`Local`** (this box's Xray gRPC API) or\n   **`Remote`** (HTTPS call to a child node, with `verify`/`skip`/`pin`/`mtls` TLS modes).\n   This is the single most important abstraction in the project. See §5.2.\n\n---\n\n## 2. Tech stack\n\n**Backend (Go 1.27):**\n\n- Web framework: **Gin** (`gin-gonic/gin`) + sessions (cookie store), gzip.\n- ORM: **GORM** with **SQLite** (default) or **PostgreSQL** (`XUI_DB_TYPE=postgres`).\n- Scheduler: **robfig/cron/v3** (seconds-precision) for all background jobs.\n- Xray: **xtls/xray-core** vendored as a library; the panel talks to the running core over\n  its **gRPC API** and also shells out to manage the process.\n- Bots: Telegram bot (**mymmrac/telego**), Discord bot (Discord REST API v10 + **gorilla/websocket** Gateway v10). i18n: **nicksnyder/go-i18n**.\n- Misc: gorilla/websocket, gopsutil (system stats), go-qrcode, gotp (2FA TOTP).\n\n**Frontend (`frontend/`):**\n\n- **React 19** + **Ant Design 6** + **Vite 8** + **TypeScript**.\n- Data layer: **TanStack Query** (`@tanstack/react-query`) over the native **Fetch API**; **Zod 4** schemas.\n- Router: **react-router 8**. Charts: **uPlot** (`frontend/src/components/viz/Sparkline.tsx`). Editor: **CodeMirror 6**.\n- **Build output goes to `internal/web/dist/`** (see `vite.config.js` → `outDir`) and is\n  embedded into the Go binary with `go:embed`. Three HTML entries: `index.html` (panel SPA),\n  `login.html`, `subpage.html`. The Go server serves the SPA; there is no separate frontend\n  deployment.\n\n**Important:** the legacy Go-template UI and `web/assets/` are **gone**. All HTML/JS comes\nfrom the embedded Vite `dist/`. Don't look for `.html` templates in `internal/web`.\n\n---\n\n## 3. Request lifecycle (follow the data)\n\n### 3.1 Admin API request (e.g. \"add a client\")\n\n```\nBrowser (React, fetch)\n  → POST {basePath}/panel/api/...\n    → Gin engine (internal/web/web.go: initRouter)\n      → middleware chain: SecurityHeaders → MaxBodyBytes (10 MiB; importDB exempt)\n                          → [DomainValidator, if webDomain set] → gzip → sessions(\"3x-ui\")\n                          → base-path/cache-control context → Localizer\n                          → API routes add: ConfigEnvelope (zstd + SHA-256) → CSRF\n        → Controller (internal/web/controller/*.go)   // HTTP concerns only: bind, validate, respond\n          → Service (internal/web/service/*.go)        // business logic + transactions\n            → GORM → DB (internal/database)            // persistence\n            → runtime.Runtime dispatch                 // apply to Xray (Local) or node (Remote)\n              → Local:  internal/xray (gRPC API or config regen + restart)\n              → Remote: internal/web/runtime/remote.go → HTTPS → child node's API\n```\n\nThe controller layer is thin. **Business logic lives in services.** When something is wrong\nwith _behavior_, the bug is almost always in a service file, not a controller.\n\n### 3.2 Subscription request (end-user fetching their config)\n\n```\nEnd user → GET {subPath}/{subId}   (separate server, internal/sub)\n  → internal/sub/controller.go (routes: raw / JSON / Clash variants, feature-flagged)\n    → internal/sub/service.go (~2.5k lines — the link/config builder)\n      → reads inbounds+clients+hosts from DB, renders per-protocol share links /\n        Clash YAML / JSON (Host rows can override address/SNI/path per inbound)\n```\n\n### 3.3 Background work (cron jobs)\n\nScheduled in `internal/web/web.go` → `startTask()`. Each job is a struct in\n`internal/web/job/`. Examples: poll Xray traffic every 5s, check client IP limits every 10s,\nnode heartbeat every 5s, periodic traffic resets (hourly/daily/weekly/monthly). See §5.4.\n\n---\n\n## 4. Directory map (what lives where)\n\n```\n3x-ui/\n├── main.go                     # Entry point: CLI (run / migrate / migrate-db / setting / cert),\n│                               #   bootstrap, signal handling, restart loop\n├── go.mod / go.sum             # Go deps (module path ends in /v3)\n│\n├── internal/                   # ALL backend Go code (private packages)\n│   ├── config/                 # Env-var config: paths, DB kind/DSN, log level, version\n│   │                           #   Every XUI_* env var is read here (config.go)\n│   ├── database/\n│   │   ├── db.go               # InitDB: connect, AutoMigrate, seeders (~1.4k lines). DB hotspot.\n│   │   ├── migrate_data.go     # Data migrations (seeders/normalizers beyond AutoMigrate)\n│   │   ├── dialect.go          # SQLite vs Postgres SQL differences\n│   │   ├── dump_sqlite.go      # DB export/backup\n│   │   └── model/              # **ALL GORM models** (model.go ~1.1k lines + siblings:\n│   │                           #   node_client_traffic.go, node_client_ip.go,\n│   │                           #   client_global_traffic.go). ⭐ Start here for data shape.\n│   ├── pia/                    # PIA WireGuard protocol client (auth, signed server list, /addKey)\n│   ├── eventbus/               # In-process pub/sub (buffered channel): outbound.down|up,\n│   │                           #   xray.crash, node.down|up, cpu.high, memory.high, login.attempt\n│   ├── tunnelmonitor/          # Optional tunnel health probe (XUI_TUNNEL_HEALTH_* env vars):\n│   │                           #   HTTP probe (default Cloudflare trace); repeated failures\n│   │                           #   trigger an Xray restart hook. Independent of panel settings.\n│   ├── xray/                   # Xray-core integration (the proxy engine wrapper)\n│   │   ├── process.go          # Spawn/supervise the Xray child process (~750 lines)\n│   │   ├── api.go              # gRPC client to a running Xray (add/remove user, stats) (~800 lines)\n│   │   ├── hot_diff.go         # ⭐ Compute minimal live changes to avoid full restart (~500 lines)\n│   │   ├── config.go           # Xray config object model\n│   │   ├── inbound.go          # Inbound JSON shaping\n│   │   ├── client_traffic.go   # ClientTraffic model (persisted as client_traffics)\n│   │   ├── traffic.go          # Traffic type helpers\n│   │   ├── log_writer.go       # Pipe Xray stdout/stderr into the panel logger\n│   │   └── geodata/            # Browse geosite/geoip .dat: streaming protowire reader,\n│   │                           #   cached category index, routing-token parsing (token.go)\n│   │\n│   ├── web/                    # The panel server\n│   │   ├── web.go              # ⭐ Server bootstrap: initRouter (all routes) + startTask (all cron jobs)\n│   │   ├── controller/         # HTTP handlers (thin). One file per resource:\n│   │   │   ├── inbound.go      #   /panel/api/inbounds\n│   │   │   ├── client.go       #   /panel/api/clients (CRUD + bulk + ips + onlines)\n│   │   │   ├── group.go        #   client-group endpoints\n│   │   │   ├── node.go         #   /panel/api/nodes   (multi-node management)\n│   │   │   ├── host.go         #   /panel/api/hosts   (per-inbound subscription host overrides)\n│   │   │   ├── server.go       #   /panel/api/server  (status, xray version, certs, logs, DB import/export)\n│   │   │   ├── setting.go      #   /panel/api/setting (settings + API tokens)\n│   │   │   ├── xray_setting.go #   /panel/api/xray    (raw Xray config editor, WARP/Nord/PIA, geodata)\n│   │   │   ├── api.go          #   /panel/api gateway (token auth, envelope + CSRF wiring)\n│   │   │   ├── index.go        #   login/logout/csrf/2FA\n│   │   │   ├── spa.go          #   SPA fallback for /panel UI routes\n│   │   │   └── websocket.go    #   WS upgrade endpoint\n│   │   ├── service/            # ⭐⭐ Business logic. This is where most real work happens.\n│   │   │   ├── inbound.go              # Inbound CRUD core (~1.4k lines)\n│   │   │   ├── inbound_node.go         # ⭐ Node sync for inbounds: reconcile, traffic merge (~1.1k lines)\n│   │   │   ├── inbound_traffic.go      # Per-client traffic accounting (~1.1k lines)\n│   │   │   ├── inbound_clients.go      # Client-within-inbound operations\n│   │   │   ├── inbound_sublink.go      # Inbound-level subscription link helpers\n│   │   │   ├── inbound_migration.go    # Inbound schema/format migrations\n│   │   │   ├── client_crud.go          # Client create/read/update/delete\n│   │   │   ├── client_bulk.go          # Bulk client ops (~1.6k lines)\n│   │   │   ├── client_inbound_apply.go # ⭐ Apply client changes to runtime (Local/Remote) (~1.2k lines)\n│   │   │   ├── client_groups.go        # Client grouping\n│   │   │   ├── client_link.go          # Per-client share-link generation\n│   │   │   ├── client_external_link.go # External links attached to clients\n│   │   │   ├── client_wireguard.go     # WireGuard client specifics\n│   │   │   ├── client_paging.go        # Server-side pagination/sort/filter for client lists\n│   │   │   ├── node.go                 # ⭐ NodeService: CRUD, probe, heartbeat, dirty-tracking (~1.1k lines)\n│   │   │   ├── node_mtls.go            # Node mTLS certificate management (master side)\n│   │   │   ├── node_tree.go            # Node hierarchy / descendants\n│   │   │   ├── host.go                 # Host rows (subscription output overrides)\n│   │   │   ├── server.go               # ServerService: status, certs, xray install, DB ops (~2.2k lines)\n│   │   │   ├── setting.go              # SettingService: all panel settings + defaults (~1.3k lines)\n│   │   │   ├── setting_mtls.go         # mTLS settings (node hardening)\n│   │   │   ├── traffic_writer.go       # Batched persistence of traffic deltas to the DB\n│   │   │   ├── xray.go                 # ⭐ XrayService: config gen + restart/hot-apply (~1.2k lines)\n│   │   │   ├── xray_setting.go         # Raw Xray config persistence\n│   │   │   ├── geodata.go              # Geo database browsing + routing-token validation\n│   │   │   ├── xray_metrics.go         # Xray observability metrics\n│   │   │   ├── metric_history.go       # Historical system/xray metrics\n│   │   │   ├── reality_scan.go         # REALITY target scanner\n│   │   │   ├── url_safety.go           # Outbound URL validation (SSRF guards)\n│   │   │   ├── outbound_subscription.go# Outbound subscription (e.g. Warp/Nord provider configs)\n│   │   │   ├── port_conflict.go        # Detect inbound port collisions\n│   │   │   ├── fallback.go             # Xray fallback (SNI/ALPN routing on shared port)\n│   │   │   ├── email/                  # Email notification service (SMTP)\n│   │   │   ├── integration/            # External providers: warp.go, nord.go, pia.go\n│   │   │   ├── outbound/               # Outbound config service\n│   │   │   ├── panel/                  # Cross-cutting panel services:\n│   │   │   │   ├── panel.go            #   panel-level helpers\n│   │   │   │   ├── user.go             #   admin user auth (bcrypt)\n│   │   │   │   ├── api_token.go        #   API token CRUD (SHA-256 hashed)\n│   │   │   │   └── websocket.go        #   WS hub / push service\n│   │   │   ├── discord/                # Discord bot client, Gateway v10, and subscriber\n│   │   │   └── tgbot/                  # Telegram bot command handlers\n│   │   ├── runtime/            # ⭐⭐ The Local/Remote node abstraction (see §5.2)\n│   │   │   ├── runtime.go      #   the Runtime interface (the contract)\n│   │   │   ├── local.go        #   Local impl → this box's Xray gRPC API\n│   │   │   ├── remote.go       #   Remote impl → HTTPS calls to a child node\n│   │   │   ├── tls_client.go   #   per-node HTTP client: verify / skip / pin / mtls\n│   │   │   └── manager.go      #   RuntimeFor(nodeID) → picks Local or Remote\n│   │   ├── job/               # Cron job structs (one file per job — see §5.4)\n│   │   ├── middleware/        # Gin middleware: security.go (headers/HSTS), bodylimit.go,\n│   │   │                      #   domainValidator.go, validate.go (CSRF), config_envelope.go\n│   │   ├── global/           # Global singletons: web server + sub server handles, restart hook\n│   │   ├── network/          # Custom net listeners (e.g. proxy-protocol aware)\n│   │   ├── session/          # Session/cookie helpers\n│   │   ├── websocket/        # WS hub implementation\n│   │   ├── locale/ + translation/  # i18n middleware + 13 locale JSON catalogs\n│   │   ├── entity/           # Shared request/response DTOs\n│   │   └── dist/             # ⚠️ Vite build output, embedded via go:embed (generated — do not hand-edit)\n│   │\n│   ├── sub/                   # The subscription server (separate from panel)\n│   │   ├── sub.go             #   server bootstrap\n│   │   ├── controller.go      #   routes for raw / JSON / Clash subscription formats\n│   │   ├── service.go         # ⭐ The link/config builder (~2.5k lines — share-link logic lives here)\n│   │   ├── json_service.go    #   JSON subscription format\n│   │   ├── clash_service.go   #   Clash/Mihomo YAML format\n│   │   ├── clash_external.go  #   external Clash config integration\n│   │   ├── external_subscription.go / external_config.go  # external sub import/aggregation\n│   │   ├── host_sub.go        #   Host-row overrides applied to subscription output\n│   │   ├── endpoint.go        #   subscription endpoint configuration\n│   │   ├── vless_route.go     #   VLESS route shaping\n│   │   ├── remark_vars.go     #   remark variable expansion\n│   │   └── links.go           #   link helpers\n│   │\n│   ├── mtproto/              # Embedded MTProto (Telegram) proxy: manager.go + per-OS\n│   │                         #   process supervision + orphan cleanup\n│   ├── logger/              # App logger (op/go-logging + lumberjack rotation)\n│   └── util/                # Leaf helpers (no business logic):\n│       ├── common/          #   errors, misc\n│       ├── crypto/          #   key/cert generation (x25519, ML-KEM/ML-DSA, ECH)\n│       ├── link/            #   outbound share-link building primitives\n│       ├── wirecodec/ + wireguard/  # WireGuard codec + integration helpers\n│       └── random/, json_util/, reflect_util/, sys/, netproxy/, netsafe/, ldap/\n│\n├── frontend/                 # React SPA (built into internal/web/dist)\n│   ├── vite.config.js        # ⭐ Build config: outDir → ../internal/web/dist, dev on :5173\n│   │                         #   (strict) proxying to :2053, entries index/login/subpage.html\n│   ├── package.json          # scripts: dev / build / preview / lint / typecheck / test / gen\n│   └── src/\n│       ├── main.tsx / routes.tsx / queryClient.ts   # SPA entry, router, query client\n│       ├── entries/          # Extra HTML entry points: login.tsx, subpage.tsx\n│       ├── pages/            # ⭐ Route screens. Mirrors the panel's feature areas:\n│       │   ├── inbounds/     #   inbound list + the big inbound form (protocols/security/transport)\n│       │   ├── clients/      #   client management screens\n│       │   ├── nodes/        #   multi-node UI\n│       │   ├── hosts/        #   subscription host-override UI\n│       │   ├── xray/         #   raw Xray config UI (routing, dns, outbounds, balancers, overrides)\n│       │   ├── index/        #   dashboard/home\n│       │   └── settings/, groups/, sub/, login/, api-docs/\n│       ├── api/              # ⭐ Data layer: http-init, QueryProvider, queryKeys, websocket bridge\n│       │   └── queries/      #   TanStack Query hooks (useNodesQuery, useStatusQuery, …)\n│       ├── schemas/          # Zod schemas: protocols, forms, api, primitives\n│       ├── generated/        # ⚠️ GENERATED from Go (see §5.5): schemas.ts, types.ts, zod.ts, examples.ts\n│       ├── components/       # Reusable UI (clients/ form/ geodata/ ui/ viz/ feedback/ utility/)\n│       ├── lib/              # Frontend domain logic (xray/ inbounds/ clients/)\n│       ├── hooks/, models/, layouts/, i18n/, utils/, styles/\n│       └── test/             # Vitest + golden fixtures (config-generation snapshot tests)\n│\n├── tools/openapigen/         # ⭐ Go program that emits frontend/src/generated/* from Go types (§5.5)\n├── docs/                     # Markdown docs (this file, custom-subscription-templates.md, …)\n├── media/                    # README images\n│\n├── Dockerfile / docker-compose.yml / DockerEntrypoint.sh / DockerInit.sh   # Container build/run\n├── install.sh / update.sh / x-ui.sh                        # VPS install + management CLI\n├── x-ui.service.*  / x-ui.rc                               # systemd units (debian/rhel/arch) + rc script\n├── windows_files/                                          # Windows service support\n└── .github/workflows/        # CI: ci.yml, codeql.yml, docker.yml, release.yml,\n                              #     mutation.yml, cleanup_caches.yml, claude-pr-review.yml,\n                              #     claude-issue-analyst.yml\n```\n\n---\n\n## 5. Cross-cutting subsystems (the parts that span many files)\n\n### 5.1 DB → Xray config pipeline (config generation & application)\n\nThe panel never edits Xray's running config directly from controllers. The flow is:\n\n1. A service mutates DB state (inbound/client/setting).\n2. `XrayService` (`service/xray.go`) builds a fresh `xray.Config` from DB state\n   (`GetXrayConfig`).\n3. It tries a **hot apply** (`tryHotApply` → `xray/hot_diff.go`): diff old vs new config and\n   push only the deltas over the Xray gRPC API (add/remove inbound, add/remove user) — **no\n   process restart**, so live connections survive.\n4. If the diff isn't hot-applicable (structural change), it falls back to a **full restart**\n   of the Xray process (`xray/process.go`).\n\nRestart is debounced via an atomic \"need restart\" flag (`SetToNeedRestart` /\n`IsNeedRestartAndSetFalse`), consumed by a `@every 30s` cron task registered in `startTask()`\n— any number of mutations inside the window causes at most one restart.\n\n**Key files:** `service/xray.go` (orchestration), `xray/hot_diff.go` (the diff algorithm),\n`xray/process.go` (process lifecycle), `xray/api.go` (gRPC calls), `xray/config.go` (config model).\n\n### 5.2 Runtime abstraction — Local vs Remote (multi-node) ⭐ most important\n\nA \"node\" (`model.Node`) is another 3x-ui instance this panel controls. Every state-changing\ninbound/client operation goes through the `runtime.Runtime` interface so the _same service\ncode_ works whether the target is the local Xray or a remote node.\n\n- **Interface:** `internal/web/runtime/runtime.go` — `Name`, `AddInbound`, `DelInbound`,\n  `UpdateInbound`, `AddUser`, `RemoveUser`, `UpdateUser`, `DeleteUser`, `AddClient`,\n  `RestartXray`, `ResetClientTraffic`, `ResetInboundTraffic`, `ResetAllTraffics`.\n- **`Local`** (`local.go`): calls this box's Xray gRPC API directly.\n- **`Remote`** (`remote.go`): serializes the operation and sends it over HTTPS to the child\n  node's API.\n- **TLS modes** (`tls_client.go`, per-node `TlsVerifyMode`):\n  `verify` (system CAs, default) / `skip` (no validation) / `pin` (leaf cert SHA-256 must\n  match `PinnedCertSha256`) / `mtls` (master presents a client certificate; node cert checked\n  against system roots; API token optional). Master-side cert management:\n  `service/node_mtls.go` + `service/setting_mtls.go`.\n- **Dispatch:** `manager.go` → `Manager.RuntimeFor(nodeID *int)`; `nil` nodeID → `Local`,\n  otherwise a cached/lazy-loaded `Remote`. `InvalidateNode(id)` drops a cached remote client.\n\nBearer tokens use the narrowest scope that matches the caller's duties:\n\n| Scope        | Access                                                                 |\n| ------------ | ---------------------------------------------------------------------- |\n| `monitor`    | Read-only status, version, and metric endpoints.                       |\n| `node-sync`  | Node configuration, inbound/client synchronization, traffic, and Xray. |\n| `node-admin` | All `node-sync` access plus `POST /panel/api/server/updatePanel`.      |\n| `admin`      | Every panel API endpoint; intended for fully trusted automation.       |\n\n`node-admin` remains a node synchronization identity when inbound rules are applied. This\npreserves the node-specific validation path while keeping software-update authority separate\nfrom ordinary `node-sync` credentials. On an mTLS connection, only a valid `node-admin` bearer\nelevates the request; other bearer scopes retain the historical mTLS `node-sync` behavior.\nExisting tokens retain their stored scope, and legacy tokens without one continue to migrate to\n`admin`.\n\n**Node identity & attribution (the hard part).** Inbounds carry a `NodeID` _and_ an\n`OriginNodeGuid`. Because inbounds can be pushed across hops, the panel attributes traffic and\nonline clients back to the originating panel using **stable GUIDs** rather than local IDs.\nRelevant logic: `service/inbound_node.go` (`ReconcileNode`, `SetRemoteTraffic`, GUID merge,\n`synthNodeGuid`, `panelGuid`) and `service/node.go` (`effectiveNodeGuid`, heartbeat, dirty\ntracking). Node \"dirty\" flags drive an **anti-entropy reconciliation** so an offline node's\ninbound edits converge once it reconnects.\n\n**Where to look for node bugs:**\n\n- Operation not reaching a node → `runtime/remote.go` + `runtime/manager.go`.\n- Wrong traffic/online attribution across hops → `service/inbound_node.go` (GUID merge paths).\n- Node shown offline / stale status → `job/node_heartbeat_job.go` + `service/node.go` (`Probe`, `UpdateHeartbeat`).\n- Edits to an offline node not applying on reconnect → dirty/reconcile logic in `service/inbound_node.go` + `service/node.go` (`MarkNodeDirty`/`ClearNodeDirty`/`NodeSyncState`).\n- TLS/mTLS handshake failures → `runtime/tls_client.go`, `service/node_mtls.go`, `service/node.go` (`FetchCertFingerprint`).\n\n### 5.3 Traffic accounting\n\nPer-client and per-inbound up/down counters originate from Xray's stats API and are persisted\nto the DB. The Xray traffic job polls the core; node traffic is pulled from child nodes and\nmerged with GUID-based baselines to avoid double counting after resets.\n\n**Key files:** `service/inbound_traffic.go`, `service/traffic_writer.go`,\n`job/xray_traffic_job.go`, `job/node_traffic_sync_job.go`, `service/inbound_node.go`\n(`SetRemoteTraffic` / `upsertNodeBaseline`), models `xray.ClientTraffic`,\n`model.NodeClientTraffic`, `model.ClientGlobalTraffic` (cross-master totals).\nA client reset is queued per hosting node in `model.NodePendingReset` (`service/node_reset_queue.go`)\nand replayed by the node sync until the node accepts it.\nPeriodic resets: `job/periodic_traffic_reset_job.go` (keyed off `Inbound.TrafficReset`).\n\n### 5.4 Background jobs (cron)\n\nAll registered in `web.go` → `startTask()`. Each is a struct with a `Run()` method in `internal/web/job/`:\n\n| Schedule            | Job                                                                                              | Purpose / condition                                                                   |\n| ------------------- | ------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------- |\n| `@every 1s`         | `check_xray_running_job`                                                                         | Restart Xray if it died (2 consecutive down checks)                                   |\n| `@every 30s`        | (inline func in `startTask`)                                                                     | Debounced Xray restart — consumes the \"need restart\" flag (§5.1)                      |\n| `@every 5s`         | `xray_traffic_job`                                                                               | Pull traffic stats from Xray (5s start delay)                                         |\n| `@every 5s`         | `node_heartbeat_job`                                                                             | Probe child nodes (online/offline)                                                    |\n| `@every 5s`         | `node_traffic_sync_job`                                                                          | Pull + merge node traffic; push reconciliation                                        |\n| `@every 10s`        | `check_client_ip_job`                                                                            | Enforce per-client IP limits                                                          |\n| `@every 10s`        | `mtproto_job`                                                                                    | Reconcile `mtg` sidecars against enabled MTProto inbounds                             |\n| `@every 10s`        | `amneziawg_job`                                                                                  | Reconcile embedded AmneziaWG interfaces against enabled local inbounds                |\n| `@every 5m`         | `outbound_subscription_job`                                                                      | Refresh outbound provider configs                                                     |\n| `@every 10m`        | `clear_logs_job` (`PruneXrayLogsJob`)                                                            | Truncate Xray access/error logs once either exceeds 64 MiB                            |\n| `@hourly`           | `warp_ip_job`, `periodic_traffic_reset_job(\"hourly\")`                                            | WARP IP rotation; traffic resets                                                      |\n| `@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       |\n| `@weekly`           | `periodic_traffic_reset_job(\"weekly\")`                                                           | Weekly traffic resets                                                                 |\n| default `@every 1m` | `ldap_sync_job`                                                                                  | Only if LDAP enabled; schedule configurable                                           |\n| default `@daily`    | `stats_notify_job`                                                                               | Only if TG bot enabled; schedule configurable                                         |\n| default `@daily`    | `discord_notify_job`                                                                             | Only if Discord bot enabled; schedule configurable                                    |\n| `@every 2m`         | `check_hash_storage`                                                                             | Only if TG bot enabled; expires bot callback hashes                                   |\n| `@every 1m`         | `check_cpu_usage`                                                                                | Only if a CPU alarm is configured (TG, Discord, or email); publishes `cpu.high`       |\n| `@every 1m`         | `check_memory_usage`                                                                             | Only if a memory alarm is configured (TG, Discord, or email); publishes `memory.high` |\n| configurable        | `free_os_memory`                                                                                 | Only if `sys.MemoryReleaseIntervalMinutes() > 0`; returns heap to OS                  |\n\nTo change _when_ something runs, edit `startTask()`. To change _what_ it does, edit the job file.\n\n### 5.5 Type generation (Go → TypeScript) ⚠️ don't hand-edit generated files\n\nThe Go backend is the schema source of truth. `tools/openapigen` (a Go program, with a\n`StructAllow` allowlist of exported types) emits\n`frontend/src/generated/{schemas,types,zod,examples}.ts`. The frontend build runs this first:\n\n- `npm run gen:zod` → `go run ./tools/openapigen` (regenerate from Go)\n- `npm run gen:api` → builds the OpenAPI doc (`scripts/build-openapi.mjs`, driven by the\n  hand-maintained endpoint registry `src/pages/api-docs/endpoints.ts`)\n- `npm run build` runs `gen:api` then `vite build`.\n\n**Implication:** if you change a Go model/DTO that crosses the API boundary, regenerate the\nfrontend types (`cd frontend && npm run gen`) instead of editing `src/generated/` by hand.\n\n### 5.6 Share-link / subscription generation\n\nTwo distinct code paths produce client configs:\n\n- **Per-client links in the panel** (the \"copy link\" / QR in the UI): `service/client_link.go`\n  - `util/link/outbound.go`.\n- **Subscription endpoint** (what a client app polls): `internal/sub/service.go` (raw links),\n  `internal/sub/json_service.go` (JSON), `internal/sub/clash_service.go` (Clash YAML).\n  **`Host` rows** (`model.Host`, edited under /panel/api/hosts) override address/SNI/path/\n  security per inbound in subscription output — applied in `sub/host_sub.go`.\n\nBoth paths must agree per protocol. A malformed link for a specific protocol/transport combo\n(e.g. XHTTP + Reality) is usually a field-lookup mismatch in **`internal/sub/service.go`** (and\nits tests `service_test.go` / golden fixtures), or in `util/link/outbound.go`. The frontend\nalso has protocol schemas under `frontend/src/schemas/protocols/` and `frontend/src/lib/xray/`.\n\n### 5.7 Event bus (in-process pub/sub)\n\n`internal/eventbus/` is a minimal buffered-channel pub/sub. Producers call a non-blocking\n`Publish(Event)`; all subscribers receive every event. Event types: `outbound.down|up`,\n`xray.crash`, `node.down|up`, `cpu.high`, `memory.high`, `login.attempt`, with structured\npayloads (OutboundHealthData, NodeHealthData, LoginEventData, SystemMetricData). Producers\ninclude the CPU/memory jobs, node heartbeat, and login handling; consumers include the\nTelegram bot, the Discord bot (`service/discord/`), and the email notifier (`service/email/`). Use it for cross-cutting\nnotifications instead of importing notification services into producers.\n\n### 5.8 Tunnel health monitor\n\n`internal/tunnelmonitor/` is an optional watchdog configured **only via env vars**\n(`XUI_TUNNEL_HEALTH_*`, read in `internal/config/`), deliberately independent of panel\nsettings so it can be enabled from a systemd `EnvironmentFile` even when the panel is\nunreachable. It periodically probes an HTTP URL (default: Cloudflare trace endpoint) through\nthe tunnel; after N successive failures (default 3) it fires a recovery callback wired to an\nXray restart.\n\n---\n\n## 6. Data model cheat-sheet\n\nGORM models in `internal/database/model/` (main file `model.go` + siblings); all registered\nfor AutoMigrate in `internal/database/db.go`.\n\n| Model                           | Table role                                | Notable fields                                                                                                                                                     |\n| ------------------------------- | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| `User`                          | Admin login                               | bcrypt password, `LoginEpoch` (invalidates sessions)                                                                                                               |\n| `Inbound`                       | An Xray inbound                           | `Tag` (unique), `Port`, `Protocol`, `Settings`/`StreamSettings`/`Sniffing` (JSON), `Enable`, `TrafficReset`, `NodeID`, **`OriginNodeGuid`**, `ClientStats` (assoc) |\n| `Client`                        | In-memory client view                     | UUID/email/flow/limits (parsed from inbound JSON; not persisted)                                                                                                   |\n| `ClientRecord`                  | Persisted client (`clients`)              | `Email` (unique), `SubID`, `UUID`, `TotalGB`, `ExpiryTime`, `LimitIP`, `Group`, `Reset`                                                                            |\n| `ClientGroup` / `ClientInbound` | Grouping + client↔inbound join            | many-to-many wiring, `FlowOverride`                                                                                                                                |\n| `ClientExternalLink`            | Extra links attached to a client          | `Kind`, `Value`, `Remark`, `SortIndex`                                                                                                                             |\n| `Host`                          | Subscription host overrides (per inbound) | `Address`, `Port`, `Sni`, `Path`, `Security`, `Fingerprint`, `SortOrder`, visibility/exclusion flags                                                               |\n| `Node`                          | A managed child panel                     | `Guid`, `Address`, `Status`, `TlsVerifyMode`, `PinnedCertSha256`, `ConfigDirty`, version/heartbeat/metric fields                                                   |\n| `NodeClientTraffic`             | Per-node client traffic baseline          | cross-node merge (anti-double-count)                                                                                                                               |\n| `NodePendingReset`              | Client resets a node has not confirmed    | `NodeId`, `Email`, `QueuedAt`; replayed by the node sync, freezes that client's node verdict until delivered                                                       |\n| `NodeClientIp`                  | Per-node client IP attribution            | `NodeGuid`, `Email`, `Ips`                                                                                                                                         |\n| `ClientGlobalTraffic`           | Cross-master usage totals                 | `MasterGuid`, `Email`, `Up`, `Down`                                                                                                                                |\n| `xray.ClientTraffic`            | Per-client counters (`client_traffics`)   | `Email`, `Up`, `Down`, `Total`, `ExpiryTime`, `LastOnline`                                                                                                         |\n| `InboundClientIps`              | IP set per client email                   | drives IP-limit enforcement                                                                                                                                        |\n| `OutboundTraffics`              | Outbound counters                         | per outbound tag                                                                                                                                                   |\n| `OutboundSubscription`          | External provider subs                    | Warp/Nord style                                                                                                                                                    |\n| `Setting`                       | Key/value panel settings                  | everything configurable                                                                                                                                            |\n| `ApiToken`                      | REST API tokens                           | SHA-256 hash (plaintext shown once)                                                                                                                                |\n| `InboundFallback`               | Fallback routing on a shared port         | SNI/ALPN/path → dest                                                                                                                                               |\n| `HistoryOfSeeders`              | Seeder bookkeeping                        | prevents re-running one-off migrations                                                                                                                             |\n\n---\n\n## 7. Symptom → File index (start here when debugging)\n\n| Symptom / task                                                                    | Primary file(s)                                                              | Then check                                                                                          |\n| --------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- |\n| 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`                            |\n| **Inbound** create/update/delete behavior                                         | `service/inbound.go`, `service/inbound_clients.go`                           | `runtime/*`, `service/xray.go`                                                                      |\n| **Client** CRUD / limits / expiry                                                 | `service/client_crud.go`, `service/client_inbound_apply.go`                  | model `ClientRecord`, `service/inbound_traffic.go`                                                  |\n| **Bulk** client operations slow/wrong                                             | `service/client_bulk.go`                                                     | `service/client_paging.go`                                                                          |\n| Xray **won't apply** a config change                                              | `service/xray.go` (`RestartXray`, `tryHotApply`)                             | `xray/hot_diff.go`, `xray/process.go`                                                               |\n| Xray **restarts when it shouldn't** (kills connections)                           | `xray/hot_diff.go` (diff not classified as hot)                              | `service/xray.go`                                                                                   |\n| **Traffic** counts wrong / reset behavior                                         | `service/inbound_traffic.go`, `job/xray_traffic_job.go`                      | `service/traffic_writer.go`, `job/periodic_traffic_reset_job.go`                                    |\n| **Node** operation not propagating                                                | `runtime/remote.go`, `runtime/manager.go`                                    | `service/inbound_node.go`                                                                           |\n| **Multi-hop / cross-node attribution** (traffic or online clients on wrong panel) | `service/inbound_node.go` (GUID merge, `synthNodeGuid`, `effectiveNodeGuid`) | `service/node.go`, model `OriginNodeGuid`/`Node.Guid`                                               |\n| Node stuck **offline / stale**                                                    | `job/node_heartbeat_job.go`, `service/node.go` (`Probe`, `UpdateHeartbeat`)  | `runtime/tls_client.go` (TLS verify)                                                                |\n| Node **TLS / mTLS** auth failures                                                 | `runtime/tls_client.go`, `service/node_mtls.go`, `service/setting_mtls.go`   | `service/node.go` (`FetchCertFingerprint`)                                                          |\n| Offline node edits **not reconciling** on reconnect                               | `service/inbound_node.go` (`ReconcileNode`, dirty flags)                     | `service/node.go` (`MarkNodeDirty`/`NodeSyncState`)                                                 |\n| **Share link / QR** malformed (per protocol)                                      | `service/client_link.go`, `util/link/outbound.go`                            | `frontend/src/lib/xray/`, `frontend/src/schemas/protocols/`                                         |\n| **Subscription** output wrong (raw/JSON/Clash)                                    | `internal/sub/service.go`                                                    | `sub/json_service.go`, `sub/clash_service.go`, sub golden tests                                     |\n| Subscription **host overrides** not applied                                       | `service/host.go`, `sub/host_sub.go`                                         | model `Host`, `frontend/src/pages/hosts/`                                                           |\n| **External subscription** import/aggregation                                      | `sub/external_subscription.go`, `sub/external_config.go`                     | `sub/clash_external.go`                                                                             |\n| **Settings** not saving / defaults                                                | `service/setting.go`, `controller/setting.go`                                | model `Setting`                                                                                     |\n| **Login / 2FA / sessions / CSRF**                                                 | `controller/index.go`, `service/panel/user.go`, `middleware/`                | `session/`                                                                                          |\n| **API tokens**                                                                    | `service/panel/api_token.go`, `controller/setting.go`                        | model `ApiToken`                                                                                    |\n| **Port conflict** on inbound add                                                  | `service/port_conflict.go`                                                   | `controller/inbound.go`                                                                             |\n| **Fallbacks** (shared 443, SNI routing)                                           | `service/fallback.go`, `controller/inbound.go`                               | model `InboundFallback`                                                                             |\n| **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()` |\n| **`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/`                            |\n| **Telegram bot** commands                                                         | `service/tgbot/`                                                             | `job/stats_notify_job.go`                                                                           |\n| **Discord bot** commands & reports                                                | `service/discord/`                                                           | `job/discord_notify_job.go`                                                                         |\n| **Email notifications**                                                           | `service/email/`                                                             | `internal/eventbus/` (consumers)                                                                    |\n| **CPU / memory alerts** not firing                                                | `job/check_cpu_usage.go`, `job/check_memory_usage.go`                        | `internal/eventbus/`, notifier settings in `service/setting.go`                                     |\n| Xray auto-restart on **dead tunnel**                                              | `internal/tunnelmonitor/`                                                    | `XUI_TUNNEL_HEALTH_*` in `internal/config/`                                                         |\n| **WARP / Nord / PIA** outbound integration                                        | `service/integration/warp.go` / `nord.go` / `pia.go`                         | `internal/pia/`, `frontend/src/pages/xray/overrides/`                                               |\n| **MTProto** proxy issues                                                          | `internal/mtproto/manager.go`, `mtproto/process*.go`                         | `job/mtproto_job.go`                                                                                |\n| **DB migration** / new column                                                     | `internal/database/db.go` (AutoMigrate list), `migrate_data.go`              | `model/model.go`                                                                                    |\n| **Cron schedule** changes                                                         | `web.go` → `startTask()`                                                     | the specific `job/*.go`                                                                             |\n| **CORS / security headers / HTTPS**                                               | `middleware/`, `web.go` (`initRouter`, TLS setup)                            | `config/` (env)                                                                                     |\n| **Env vars / paths / DB type**                                                    | `internal/config/config.go`                                                  | `.env.example`                                                                                      |\n| **Frontend route / screen**                                                       | `frontend/src/pages/<area>/`, `frontend/src/routes.tsx`                      | `frontend/src/api/queries/`                                                                         |\n| **Frontend ↔ backend type mismatch**                                              | regenerate: `cd frontend && npm run gen` (`tools/openapigen`)                | `frontend/src/generated/`                                                                           |\n| **System status / CPU / metrics**                                                 | `service/server.go`, `service/xray_metrics.go`, `service/metric_history.go`  | `controller/server.go`, gopsutil                                                                    |\n\n---\n\n## 8. Layering rules (where new code belongs)\n\n1. **Controllers are thin.** Only: bind/validate input, call one service, shape the HTTP\n   response. No DB queries, no Xray calls, no business rules in `controller/`.\n2. **Services own the logic and transactions.** All business rules, DB access, and decisions\n   about applying changes live in `service/`. If you're tempted to query GORM from a\n   controller, move it to a service.\n3. **Never touch Xray's running state from a controller or job directly.** Go through\n   `XrayService` / the `runtime.Runtime` interface so local vs node dispatch stays correct.\n4. **Any state-changing inbound/client o\n\n<!-- File: CLAUDE.md (Tokens: ~3611 | Category: claude-rule) -->\n# CLAUDE.md\n\nOperational guide for AI agents working in this repo. Long-form human docs:\n`CONTRIBUTING.md` (setup, testing philosophy) and `frontend/README.md`.\nRead those before large changes. This file is the short, must-follow version.\nFor a deep navigation map (request lifecycle, cron-job table, symptom → file\nindex, layering rules), read `docs/architecture.md` on demand — do not guess\nfile locations when it can answer in one hop.\n\n## Stack\n- Backend: Go 1.27 (`module github.com/mhsanaei/3x-ui/v3`), Gin, GORM.\n  Runs Xray-core as a managed child process (`internal/xray/process.go`) and\n  imports `github.com/xtls/xray-core` for config types + gRPC stats/handler/router\n  API. MTProto inbounds run a second managed child — the `mtg-multi` binary\n  (a multi-secret mtg fork — NOT a Go dependency; its prebuilt release binary is\n  fetched at image/release build time by `DockerInit.sh` + `release.yml`,\n  panel-side code in `internal/mtproto/`) — outside Xray, one process per inbound\n  serving each\n  client's FakeTLS secret via the fork's `[secrets]` section (plus per-client\n  ad-tags via `[secret-ad-tags]` and per-client data quota / expiry via\n  `[secret-limits]`, mapped from the client's `totalGB`/`expiryTime`). Client,\n  ad-tag and quota/expiry edits are hot-applied through the fork's management API\n  (`PUT /secrets`, bearer-token guarded) so connections survive; the manager\n  falls back to a process restart on older binaries. A client's panel-side\n  traffic reset also calls `POST /secrets/{name}/reset-quota` so a renewed client\n  is not re-blocked by the sidecar's quota counter.\n- Storage: SQLite by default (`/etc/x-ui/x-ui.db` on Linux; the executable dir on\n  Windows), PostgreSQL optional (`XUI_DB_TYPE` / `XUI_DB_DSN`). The CGo SQLite\n  driver (`mattn/go-sqlite3`) needs a C compiler — `CGO_ENABLED=0` builds fail.\n- Frontend: React 19 + Ant Design 6 + Vite 8 + TypeScript in `frontend/`,\n  built into `internal/web/dist/` (gitignored) and embedded via `embed.FS`.\n\n## Repo map\n- `main.go` — entry point + `x-ui` CLI (run, migrate, migrate-db, encrypt-tokens, setting, cert).\n- `internal/config/` — env parsing (XUI_DEBUG, XUI_LOG_LEVEL, XUI_LOG_FOLDER,\n  XUI_BIN_FOLDER, XUI_SKIP_HSTS, XUI_PORT, XUI_DB_*).\n- `internal/database/` + `internal/database/model/` — GORM schema (~24 models;\n  Inbound, Client, Setting, User are the core), inbound Protocol enum,\n  AutoMigrate + hand-written migrations in `db.go`.\n- `internal/xray/` — Xray child-process lifecycle, config generation, gRPC API.\n- `internal/xray/geodata/` — streaming geosite/geoip `.dat` reader (cached\n  category index + paged entries) and `geosite:`/`geoip:`/`ext:` token parsing.\n- `internal/mtproto/` — MTProto inbounds via the bundled `mtg-multi` binary.\n- `internal/tuic/` — TUIC v5 inbounds: `tuic-server` sidecar supervisor, native Go UDP relay traffic metering.\n- `internal/amneziawg/` — AmneziaWG protocol shape: instance/peer derivation\n  from an inbound, 3.1 obfuscation param generation + validation, port-forward\n  spec parsing.\n- `internal/amneziawgnet/` — embedded AmneziaWG runtime: amneziawg-go device\n  over a gVisor userspace netstack, per-inbound reconcile manager, TCP/UDP\n  relay into a loopback per-peer-auth SOCKS5 Xray inbound, port-forward\n  listeners, per-peer IPv6 egress aliases.\n- `internal/pia/` — PIA WireGuard protocol client (auth, signed server list, `/addKey`).\n- `internal/sub/` — subscription server (raw / JSON / Clash).\n- `internal/eventbus/` — in-process pub/sub (outbound/node health, xray.crash,\n  cpu.high, memory.high, login.attempt).\n- `internal/logger/`, `internal/util/` (link, crypto, sys, ldap, …),\n  `internal/tunnelmonitor/` — shared infrastructure.\n- `internal/web/` — Gin server (embeds `dist/` + `translation/`).\n  - `controller/` — panel + REST API handlers; OpenAPI at /panel/api/openapi.json.\n  - `service/` — business logic (InboundService, SettingService, XrayService,\n    node sync); subpackages tgbot/, discord/, email/, outbound/, panel/, integration/.\n  - `job/` — 19 cron jobs (traffic, fail2ban IP-limit, node heartbeat/sync, LDAP,\n    CPU/memory watchdogs, …); full table in `docs/architecture.md` §5.4.\n  - `middleware/`, `entity/`, `global/`, `session/` (CSRF), `network/`,\n    `runtime/` (master/sub-node over mTLS), `websocket/`.\n  - `locale/` + `translation/` — i18n, 13 embedded locale JSON files.\n- `frontend/` — React + TS source (see `frontend/CLAUDE.md`).\n- `tools/openapigen/` — Go generator that emits frontend types + Zod/JSON schemas\n  into `frontend/src/generated/` from Go structs. The OpenAPI doc itself\n  (`frontend/public/openapi.json`) is assembled from those + `endpoints.ts` by\n  `frontend/scripts/build-openapi.mjs`.\n- `docs/` — separate Next.js/Fumadocs site (pnpm, own CI in `docs-ci.yml`,\n  outside `make verify`). Holds a THIRD independent implementation of\n  link/subscription generation in `docs/lib/xray/` — check it whenever\n  share-link or install-command output changes.\n\n## Hard rules (non-negotiable)\n- Correct fix over small fix. Find the root cause and fix it the right way, however\n  much code that takes. Size the change by what the correct fix needs, never by\n  line count: when the right fix spans many files, or needs a migration, a shared\n  helper or a new abstraction, write it. A guard that hides the symptom while the\n  cause survives is the wrong fix, however small. Two limits remain:\n  - Everything added must be something the correct fix needs. No speculative\n    knobs, unused extension points or \"while I was here\" rewrites. Unrelated\n    refactors and cleanups go in their own commit.\n  - Stop and ask only when the right fix needs a decision the code cannot answer:\n    a deliberate user-visible behaviour change, or two sound designs with a real\n    trade-off. Ask with a recommendation. Size alone is never a reason to stop,\n    defer or ship a smaller patch.\n- Comments in committed Go/TS: 2 lines MAX per comment block. Make the name\n  carry the meaning first and rename rather than annotate; spend the 2 lines on\n  the *why* a name cannot hold — an invariant, an issue number, a non-obvious\n  constraint. Exempt: `//go:build`, `//go:generate`, and other directives.\n  HTML `<!-- -->` is fine. (A linter cannot enforce this — you must.)\n- New `g.POST`/`g.GET` in `internal/web/controller/` REQUIRES a matching entry\n  in `frontend/src/pages/api-docs/endpoints.ts`, then `make gen` (or\n  `cd frontend && npm run gen`). Hand-maintained but pinned both ways by\n  `TestRouteRegistryContract` (`internal/web/routes_contract_test.go`): a missing\n  OR stale entry fails `make test-go`. Scope: `/panel/api/*` + a few session\n  routes; sub-server routes are exempt.\n- Response examples come from Go struct `example:` tags via `tools/openapigen` —\n  never hand-write them. A new struct must be added to openapigen's `StructAllow`\n  allowlist (`tools/openapigen/main.go`) or it is silently omitted from\n  schemas/examples (and `build-openapi.mjs` then fails on the missing schema).\n- A new or renamed endpoint has a FOURTH step nothing checks: copy\n  `frontend/public/openapi.json` → `docs/public/openapi.json`, then\n  `cd docs && pnpm gen:api` to refresh the MDX under\n  `docs/content/docs/en/reference/api/`. `docs-ci.yml` fires only on `docs/**`.\n- A new English i18n key goes in EVERY locale JSON in `internal/web/translation/`\n  (13 files) AND must be referenced from `frontend/src` or Go in the SAME commit —\n  `frontend/src/test/i18n-dead-keys.test.ts` fails both ways. It is a frontend\n  test, so run `npm test`, not just `make test-go`. At runtime the frontend falls\n  back to en-US; Go (`internal/web/locale/`) returns \"\" for an unknown key.\n- DB / model changes require a migration in `internal/database/db.go`.\n- Every state-changing inbound/client op dispatches through `runtime.Runtime`\n  (`internal/web/runtime/`) — never straight to `internal/xray/api.go`, never from\n  a controller or cron job. A direct call passes every local test and silently\n  breaks every multi-node deployment. Other layering rules: `docs/architecture.md` §8.\n- Conventional commits: `type(area): short imperative summary`, then a body\n  explaining the why. Types in use: `fix`, `feat`, `chore`, `refactor`, `perf`,\n  `docs`, `style`.\n\n## Tests: TDD, and only tests that can fail (Go and frontend)\n- Work red → green → refactor.\n  - Bug: turn the reproduction into a test first, and watch it fail for the\n    reported reason.\n  - Feature: write the test for the first behaviour before writing its code.\n  - Then write the code that makes it pass, and refactor with the suite green.\n\n  If a test was written after the code, prove it anyway: revert the code, watch\n  the test go red, then restore. A test that passes either way is worse than no\n  test. It certifies nothing, and then gets cited as proof the fix works.\n- Every test must name the failure it catches. When no test can reach a change\n  (workflow YAML, pure wiring, layout), say so and name the command that\n  demonstrates it. Never write a stand-in test.\n- Fake tests are forbidden. Delete any you write or meet in the code you touch:\n  - tests of a getter, a constant, a rename, a pure map lookup, or an input the\n    function can never receive;\n  - tests that restate the implementation, such as recomputing the expected\n    value with the same formula or asserting that a mock was called exactly the\n    way the code calls it;\n  - mocking the unit under test, or mocking so much around it that the real\n    code path never runs;\n  - assertions too weak to fail: `err != nil`, `len > 0`, `toBeDefined()`, or\n    `not.toThrow()` alone;\n  - golden files or snapshots regenerated to match whatever the code now outputs;\n  - extra cases that exercise no distinct branch, and tests written to raise\n    coverage.\n\n  One real test that drives the bug through the actual code path beats five\n  that restate the code.\n\n## Go conventions\n- Stdlib `testing` only (no testify). Table-driven, `t.Run` subtests,\n  `t.Helper()` on helpers. Assert the exact value / typed error / emitted\n  string, never just `err != nil`. Prefer real deps over mocks: throwaway DB via\n  `dbtest.InitDB(t, filepath.Join(t.TempDir(), \"x-ui.db\"))`\n  (`internal/database/dbtest`: copies a once-migrated template and registers\n  `CloseDB` cleanup; a fresh `database.InitDB` costs ~7x more, ~850ms under\n  `-race`); `httptest` for HTTP. Keep `database.InitDB` for reopening a file or\n  migrating a hand-built legacy DB. `internal/sub`'s `initSubDB(t)` is the template.\n- Code must pass `golangci-lint run` (gofumpt + goimports formatting): `make lint`.\n- Postgres, xray-gRPC-e2e, master+node and scale tests `t.Skip` unless\n  `XUI_TEST_PG_DSN`, `XUI_DB_TYPE`+`XUI_DB_DSN`, `XRAY_E2E_BINARY`,\n  `XUI_NODE_E2E_BINARY` or `XUI_SCALE_TEST` is set — a green `go test ./...`\n  does not mean those paths ran.\n\n## Frontend conventions (summary; full version in frontend/CLAUDE.md)\n- Ant Design 6 only — no Tailwind/shadcn. Targeted tweaks, not rewrites.\n- TS strict; `@typescript-eslint/no-explicit-any` is an error. Zod schemas in\n  `src/schemas/` are the source of truth; infer types with `z.infer`, never\n  hand-write. Do not edit `src/generated/`.\n- Node 26 (`.nvmrc`) — `make gen` imports `.ts` directly and needs its type\n  stripping; Node 22 dies with `ERR_UNKNOWN_FILE_EXTENSION`. `npm test` includes\n  a headless-Chromium Storybook project, so run\n  `npx playwright install --with-deps chromium` once or `make verify` fails.\n- Editing `frontend/src` does NOT change what users see until the Vite build is\n  regenerated into `internal/web/dist/`. In `XUI_DEBUG=true`, HTML is served from\n  the frozen embedded FS but JS/CSS off disk — after `npm run build` you MUST\n  restart `go run .` or you get a blank page with 404s.\n- After touching share-link logic (`src/lib/xray/`), run `npm run test` (golden\n  fixtures); regenerate snapshots (`npx vitest run -u`) only for intentional\n  output changes, never to make a red test green.\n\n## Build, test, verify\nA fresh clone has no `internal/web/dist/`, so a bare `go build ./...` dies with\n`pattern all:dist: no matching files found` while ~35 other packages pass — it\nreads as a broken repo, not a missing step. Run `make dist-stub` once; every\n`make` Go target already depends on it, which is why `make test-go` beats\n`go test ./...`. Run `make help` for all targets. The local gate:\n\n    make verify   # gen-check + lint + typecheck + test + build + build-storybook\n\nThat is the *fast* gate, not all of CI. `ci.yml` also runs `make race`,\n`make vulncheck`, a live-Postgres job (where a SKIP counts as a failure),\n`make node-e2e` (a real master and node panel, `internal/nodee2e/`) and a\n30s fuzz smoke on `FuzzParseLink`/`FuzzDecodeCertPin` — run those locally when\nyou touch DB/dialect, node sync or parser code. Node sync has two layers: every\n`runtime.Remote` call gets a cell in `internal/web/node_contract_test.go` (fast,\nin `make test-go`; a method without one fails it), and a multi-tick flow (cron,\nadopt, node down) gets one in `internal/nodee2e/node_sync_test.go`.\n\nCommon targets: `make gen` (regenerate Zod/OpenAPI), `make lint` (Go + frontend),\n`make test` (Go `-shuffle=on` + frontend), `make race`, `make build`. See `Makefile`.\n\n## Definition of done (before opening a PR)\n1. `make verify` passes — its `gen-check` already runs `make gen` and fails on a\n   dirty `frontend/src/generated` / `frontend/public/openapi.json`.\n2. Diff is focused; refactors are separate from feature work.\n\n<!-- File: frontend/CLAUDE.md (Tokens: ~1150 | Category: claude-rule) -->\n# frontend/CLAUDE.md\n\nFrontend agent guide. Full detail: `frontend/README.md` and the root\n`CONTRIBUTING.md` (\"Working on the frontend\"). This is the short version.\n\n## What this is\nReact 19 + Ant Design 6 + Vite 8 + TypeScript. The Vite config is\n`vite.config.js` (plain JS). Three bundles, each emitted into\n`internal/web/dist/` and embedded into the Go binary:\n- `index.html` — admin panel SPA (entry `src/main.tsx`; react-router under\n  `/panel`, lazy routes).\n- `login.html` — login + 2FA (`src/entries/login.tsx`).\n- `subpage.html` — public subscription viewer (`src/entries/subpage.tsx`).\nThe `@` import alias maps to `src/`.\n\n## Data flow\n- Server state via TanStack Query (`src/api/`, keys in `src/api/queryKeys.ts`);\n  invalidate on mutation. WebSocket pushes feed the cache\n  (`src/api/websocketBridge.ts`).\n- Local UI state in the page (`useState`); shared concerns via `src/hooks/`.\n  Extend an existing hook before adding a global.\n- Zod (`src/schemas/`) is the single source of truth for the xray config model.\n  Infer types with `z.infer`. Go-side types are mirrored into `src/generated/`\n  by `npm run gen:zod` (`go run ./tools/openapigen`) — do not hand-edit that\n  folder (every file is marked `DO NOT EDIT`).\n- xray domain logic (links, defaults, form<->wire adapters) is pure functions in\n  `src/lib/xray/`. HTTP goes through `HttpUtil` in `src/utils/index.ts`.\n\n## Rules\n- Ant Design 6 for components; no Tailwind/shadcn (a migration was rolled back).\n  Form *state* runs on React Hook Form (`src/components/form/rhf/`), not Ant\n  Design's `Form` store.\n- Function components + hooks only; no class components.\n- Comments in committed TS/TSX: 2 lines MAX per comment block, spent on the\n  *why* a name cannot hold (same rule as root CLAUDE.md). HTML comments are fine.\n- TS strict; oxlint's `typescript/no-explicit-any` is an error. Build forms with `useZodForm` +\n  `FormField` from `@/components/form/rhf` (wrap the tree in `FormProvider`);\n  validate through the `zodResolver` or per-field\n  `rules={{ validate: rhfZodValidate(Schema.shape.field) }}` — messages are Zod\n  issue keys resolved via `t()`, never inline `z.string()`. AntD `<Form>` stays\n  only as a layout wrapper. Complex shared config editors (FinalMask / Sniffing /\n  Sockopt) remain AntD-`Form` islands wrapped as value/onChange adapters in\n  `src/lib/xray/forms/fields/`, bound via a `Controller`.\n- New `g.POST`/`g.GET` route => add it to `src/pages/api-docs/endpoints.ts`,\n  then `npm run gen`.\n- i18n strings live in `internal/web/translation/<locale>.json`, NOT under\n  `frontend/`, and are shared with the Go backend. A new English key must be\n  added to every locale. Interpolation here uses single braces `{var}`, not the\n  i18next default `{{var}}`.\n- Persian/Arabic (RTL) users are first-class — isolate code identifiers on their\n  own line when writing Persian text in labels/toasts.\n- Vite is pinned to an exact version (no `^`) — bump deliberately, then verify\n  `npm run dev` AND `npm run build`.\n\n## Adding a panel route\n1. `src/pages/<page>/<Page>.tsx` (kebab folder, PascalCase component).\n2. Register in `src/routes.tsx` under `/panel` (lazy import).\n3. Add a sidebar link in `src/layouts/AppSidebar.tsx` if it needs nav.\nOnly standalone bundles (login/subpage) need a new `.html` + `src/entries/*` +\n`rollupOptions.input` (in `vite.config.js`) + a Go controller route.\n\n## Commands\n- `npm run dev` (HMR on :5173, proxies to the Go panel on :2053 — start Go first).\n- `npm run typecheck` / `npm run lint` / `npm run test` / `npm run build`.\n- `npm run gen` = `gen:zod` (Go → `src/generated/`) + `gen:api`\n  (`build-openapi.mjs` → `public/openapi.json`).\n- `npm run storybook` (workbench on :6006) / `npm run build-storybook` (CI\n  compile-checks every story). Reusable `src/components/` get a co-located\n  `<Component>.stories.tsx` with `tags: ['autodocs']`; document props via\n  `argTypes` / `parameters.docs` string metadata, never JSDoc.\n- After `npm run build`, RESTART `go run .` (see the XUI_DEBUG gotcha in root\n  CLAUDE.md) before checking the panel.\n</yakaai_skills>"}