Files
3x-ui/CONTRIBUTING.md
T
Sanaei 92fb94d856 Move to TypeScript 7 and the oxc toolchain (oxlint + oxfmt) (#6262)
* chore(frontend,docs): move to TypeScript 7 and replace ESLint with oxlint

TypeScript 7 is the native Go port and ships no programmatic compiler
API, so typescript-eslint cannot run at all: it peer-pins
typescript >=4.8.4 <6.1.0 (canary too) and hard-crashes with
"typescript-eslint does not support TS 7.0". Upstream support is
tracked in typescript-eslint#10940 and targets TS >=7.1.

Rather than wait, or carry Microsoft's side-by-side alias (which keeps
a second TS 6 install alive purely to feed the linter), both projects
move to oxlint, which never depended on the TypeScript API.

Typecheck drops from ~9.7s to ~2.2s and 167 packages leave frontend/.

oxlint has no no-restricted-syntax, so the #6121/#6127 cleared-
InputNumber guard is reimplemented as a JS plugin in
frontend/tools/oxlint/. It was verified to still fire in
pages/settings/** and pages/xray/** and to stay exempt in *Modal.tsx.

The type-aware @deprecated sweep survives too, as
`npm run lint:deprecated`: oxlint's type-aware mode runs on
oxlint-tsgolint, which drives the TS 7 typescript-go checker, so the
TS 7 move is what makes it possible.

Behaviour is preserved rather than tightened. jsx-a11y/prefer-tag-over-role
is off in both configs because it was never part of the recommended sets
ESLint actually ran, and oxlint honours the existing eslint-disable
comments, so no source churn was needed.

Two real fixes fell out of the stricter linting:
- outbound-link-parser.test.ts used `out?.streamSettings` behind an `as`
  cast, which hid the optional chain from ESLint and would throw on a
  null parse; the rest of the file already used `out!`.
- InputAddon's conditional role/tabIndex/onKeyDown is genuinely
  accessible but oxlint cannot evaluate it, so it gets a scoped disable.

* chore(docs): replace Prettier with oxfmt

oxfmt is the oxc project's Prettier-compatible formatter, so this pairs
with the oxlint move and drops the last JS-based tool from the docs
toolchain.

The swap is behaviour-preserving. Running Prettier and oxfmt over the
same files, with the existing .prettierrc.json settings migrated via
`oxfmt --migrate=prettier`, produces byte-identical output on every
file. (Comparing them outside the project directory is misleading:
Prettier silently falls back to its defaults when it cannot find its
config, which looks like a mismatch but is not one.)

The 18 files reformatted here were already failing `pnpm format:check`
before this change — Prettier wanted the exact same edits. The check is
not part of docs-ci.yml, which is why the drift went unnoticed.

.prettierignore becomes ignorePatterns in .oxfmtrc.json, keeping the
deliberate MDX exclusion: reflowing MDX prose merges headings into
paragraphs and collapses lists inside Steps/Callout components. Both
that and the generated fumadocs-openapi reference output were verified
untouched.

oxfmt is pinned to 0.63.0 rather than latest. pnpm 11's built-in
minimumReleaseAge policy rejects same-day releases, and 0.64.0 would
have made pnpm silently append 20 waiver lines to pnpm-workspace.yaml.

* style(frontend): adopt oxfmt and format src

frontend/ has never had a formatter, so this reformats 344 of 497 files
in src/. The change is purely whitespace, quoting and line wrapping —
no logic is touched. It is kept in its own commit so it does not bury
the TypeScript 7 / oxlint migration or the git blame for the code
itself.

Settings match docs/ and the code as it was already written: single
quotes, semicolons, trailing commas, 2-space indent, 100 columns. That
was measured rather than assumed — src/ was already uniformly
single-quoted and 2-space indented, with p90 line length at 75.

Formatting is scoped to src/ (mirroring `oxlint src`) and
.oxfmtrc.json ignores src/generated. Both matter: `make gen-check`
compares src/generated and public/openapi.json, and
`make msw-worker-check` byte-compares public/mockServiceWorker.js
against the installed MSW runtime, so reformatting any of them breaks
the gate.

Reflowing also moves `eslint-disable-next-line` comments off the line
they guard, which broke two suppressions that had been silently
correct before:
- clone-inbound-modal.test.tsx: the object literal became multi-line,
  leaving `} as any;` four lines below its no-explicit-any disable.
- ClientsPage.tsx: the useMemo dependency array moved onto its own
  line, out from under its exhaustive-deps disable.
Both comments were relocated onto the line they actually guard, and
verified to still suppress by removing them and watching the errors
return.

* ci: enforce formatting in CI and make verify

Adding oxfmt in the previous two commits gave both projects a formatter
but nothing that checks it, which is how docs/ had already drifted to 18
unformatted files: docs-ci.yml runs typecheck, lint, test and build, but
never format:check, so Prettier's complaints were only ever visible to
whoever ran it by hand.

Wire `format:check` into the frontend job in ci.yml and the docs job in
docs-ci.yml, and add a `format-check` target to `make verify` so the
local gate keeps mirroring CI as the Makefile header promises.

Verified the step actually bites rather than passing vacuously: adding
a badly formatted line to a source file in each project makes both
`make format-check` and `pnpm format:check` fail, and reverting it makes
them pass again.

No workflow referenced ESLint or Prettier by name — they all invoke the
package scripts — so the tooling swap needed no other CI changes.

* ci: trigger CI on Makefile changes

The path filters listed **.go, go.mod, go.sum, frontend/**, .nvmrc and
ci.yml itself, but not the Makefile — so a change to the canonical task
runner that ci.yml is meant to mirror could land without any job
running. The previous commit, which edits both, only triggers because
it happens to touch ci.yml too.

* fix(frontend): replace deprecated Ant Design 6 APIs in the geo components

`npm run lint:deprecated` reported five uses of props Ant Design 6 has
deprecated. All five are gone, and the matching runtime warnings no
longer appear in the test output.

Tag `bordered={false}` becomes `variant="filled"` and Space `direction`
becomes `orientation`; both are the one-to-one replacements named in
antd's own deprecation messages, and `direction`/`orientation` share the
same Orientation type.

Input `addonAfter` is the one that is not a rename. It becomes a
`Space.Compact block` wrapping the Input and the browse Button, which is
antd's documented migration. `block` keeps the field filling its form
row as the addon did. Note this is a deliberate visual change: the
button used to be a borderless `type="text"` icon sitting inside the
addon's grey box, and is now a regular button whose border joins the
input. The tooltip, aria-label, ref, id and onBlur wiring are unchanged,
so the react-hook-form binding in RuleFormModal and the existing tests
still address it the same way.

Only these five were deprecated. The other `bordered` props in the tree
sit on QRCode, Table, Descriptions and Alert, where the prop is not
deprecated, and these were the only two Space `direction` uses in the
codebase.

* fix(frontend): restore lint rules lost in the oxlint migration, and test the guard

Addresses the review on #6262.

The frontend config re-enabled only no-explicit-any and no-unused-vars
and left the rest of tseslint's recommended set to oxlint's correctness
category. It does not cover all of it. Confirmed by linting one probe
file against both configs: docs/ (which enumerates the rules) reports
all nine, frontend/ reported four. So ban-ts-comment,
no-empty-object-type, no-namespace, no-require-imports and
no-unsafe-function-type had silently stopped being enforced — a `//
@ts-ignore` or a `namespace` block would have landed unflagged. The ten
rules are now mirrored from docs/.oxlintrc.json, and src/ still passes.

The #6121/#6127 guard was 57 lines of hand-written AST walking with no
test. It now has one: fixtures for the three banned shapes plus an
onNumber()-wrapped control, asserting the rule fires three times and
that .oxlintrc.json still wires it to the right paths. Verified it fails
for the right reason by making walk() enumerate nothing, which is the
silent-death mode the review described — the traversal depends on
Object.keys() seeing AST children as own enumerable properties.

The fixtures deliberately violate the rule, so their oxlint config is
named guard.oxlintrc.json rather than .oxlintrc.json: oxlint discovers
nested configs by directory, which would otherwise turn the fixtures
into three lint errors. The test passes it explicitly with -c.

Also from the review:
- lint and format now cover tools/ as well as src/, so the one piece of
  hand-written lint logic in the repo is no longer the least covered
  file in it.
- lint-staged runs oxfmt before oxlint --fix. Formatting became a hard
  CI gate in this PR while the hook only ran the linter, so a commit
  could pass the hook and fail CI on formatting alone.
- .oxfmtrc.json ignores public/, so the artefacts that make gen-check
  and make msw-worker-check byte-compare stay safe even if oxfmt is
  invoked without a path argument.
- The MDX and generated-reference rationales that .prettierignore
  carried are back as comments in docs/.oxfmtrc.json — oxlint and oxfmt
  both accept JSONC, so relocating them was unnecessary.

Not applied: the review also suggested restoring ../internal/web/dist to
the ignore lists. Both tools reject `..` patterns outright ("patterns
are resolved within the config file's directory"), and being outside
frontend/ it is unreachable anyway.
2026-08-19 15:36:27 +02:00

21 KiB

Contributing

Thanks for taking the time to contribute to 3x-ui. This guide gets a development panel running locally and explains the conventions the project follows so changes land cleanly.

Prerequisites

  • Go 1.26+ (the version pinned in go.mod)
  • Node.js 24 LTS (the version pinned in .nvmrc) and npm 10+ (for the React frontend)
  • Git
  • A C compiler — required by the CGo SQLite driver (github.com/mattn/go-sqlite3). Linux and macOS already ship one; for Windows see below.

Windows: MinGW-w64

go build on Windows fails with cgo: C compiler "gcc" not found until a GCC toolchain is installed. Two options — pick whichever fits.

Option A — standalone zip (fastest, no package manager)

  1. Download the latest build from https://github.com/niXman/mingw-builds-binaries/releases. For most setups, pick a release named:
    x86_64-<version>-release-posix-seh-ucrt-rt_<n>-rev<m>.7z
    
    (64-bit, POSIX threads, SEH exceptions, UCRT runtime — matches modern Windows defaults.)
  2. Extract it somewhere stable, e.g. C:\mingw64\.
  3. Add C:\mingw64\bin to the Windows PATH (System Properties → Environment Variables → Path → New).
  4. Open a fresh terminal and confirm:
    gcc --version
    

Option B — MSYS2 (when a Unix shell is also useful)

  1. Install MSYS2 from https://www.msys2.org/.
  2. Open the MSYS2 UCRT64 shell from the Start menu and update once:
    pacman -Syu
    
  3. Install the UCRT64 toolchain:
    pacman -S --needed mingw-w64-ucrt-x86_64-gcc mingw-w64-ucrt-x86_64-pkg-config
    
  4. Add C:\msys64\ucrt64\bin to the Windows PATH.
  5. Verify with gcc --version in a fresh terminal.

After either path, go build ./... and go run . work normally.

Why MinGW-w64 over MSVC: mattn/go-sqlite3 officially supports GCC, builds are faster on Windows, and the toolchain does not require a Visual Studio install. If Visual Studio Build Tools are already present that works too — just make sure CC=cl is not set in the environment.

Cross-building the Linux SQLite target from Windows (or vice versa) requires a separate cross-compiler and is out of scope here; build natively on the target OS.

First-time setup

git clone https://github.com/MHSanaei/3x-ui.git
cd 3x-ui

cp .env.example .env

mkdir x-ui

go mod download

cd frontend
npm install
npm run build
cd ..

.env.example ships with defaults that keep the database, logs, and xray binary inside the local x-ui/ folder so nothing escapes the project directory:

XUI_DEBUG=true
XUI_DB_FOLDER=x-ui
XUI_LOG_FOLDER=x-ui
XUI_BIN_FOLDER=x-ui
XUI_INIT_WEB_BASE_PATH=/
# XUI_PORT=8080

Drop the xray binary (xray-windows-amd64.exe on Windows, xray-linux-amd64 on Linux, etc.) plus the matching geoip.dat and geosite.dat files into x-ui/. The easiest source is a released Xray-core build. On Windows, wintun.dll is also required for testing TUN inbounds.

Running

go run .

Open http://localhost:2053 and log in with admin / admin. Credentials must be changed on first login.

Inside VS Code

The repo checks in two VS Code launch profiles in .vscode/launch.json: Run 3x-ui (Debug) for the default SQLite setup, and Run 3x-ui (Postgres) which points XUI_DB_TYPE/XUI_DB_DSN at a local PostgreSQL. The Postgres profile also prepends the PostgreSQL bin to PATH so the panel can find pg_dump/pg_restore (the postgresql-client tools used for DB backup/restore) — adjust the DSN and that path to your machine:

{
  "$schema": "vscode://schemas/launch",
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Run 3x-ui (Debug)",
      "type": "go",
      "request": "launch",
      "mode": "auto",
      "program": "${workspaceFolder}",
      "cwd": "${workspaceFolder}",
      "env": {
        "XUI_DEBUG": "true",
        "XUI_DB_FOLDER": "x-ui",
        "XUI_LOG_FOLDER": "x-ui",
        "XUI_BIN_FOLDER": "x-ui"
      },
      "console": "integratedTerminal"
    },
    {
      "name": "Run 3x-ui (Postgres)",
      "type": "go",
      "request": "launch",
      "mode": "auto",
      "program": "${workspaceFolder}",
      "cwd": "${workspaceFolder}",
      "env": {
        "XUI_DEBUG": "true",
        "XUI_LOG_FOLDER": "x-ui",
        "XUI_BIN_FOLDER": "x-ui",
        "XUI_DB_TYPE": "postgres",
        "XUI_DB_DSN": "postgres://xui:xuipass@127.0.0.1:5432/xui?sslmode=disable",
        "PATH": "C:\\Program Files\\PostgreSQL\\18\\bin;${env:PATH}"
      },
      "console": "integratedTerminal"
    }
  ]
}

Working on the frontend

The panel UI is a React 19 + Ant Design 6 + TypeScript app under frontend/, built with Vite 8. The sections below cover the architecture, the conventions, and the two dev workflows.

Architecture

The frontend ships three Vite bundles, each emitted into internal/web/dist/ and embedded into the Go binary at compile time via embed.FS:

  • index.html — the admin panel, a single-page app. src/main.tsx mounts a react-router createBrowserRouter (see src/routes.tsx) under the /panel basename; every route (/panel, /panel/inbounds, /panel/clients, /panel/groups, /panel/nodes, /panel/settings, /panel/xray, /panel/api-docs) is lazy-loaded inside a shared PanelLayout (sidebar + header + <Outlet>).
  • login.html — the login + 2FA screen (src/entries/login.tsx), a standalone bundle.
  • subpage.html — the public subscription viewer (src/entries/subpage.tsx), a standalone bundle.

Panel navigation happens client-side through React Router, and per-route code is lazy-split so the initial panel load stays small. login and subpage stay separate documents because they are reached without an authenticated panel session.

State and data flow

  • Server state via TanStack Query. API reads go through @tanstack/react-query (QueryProvider in src/main.tsx, keys in src/api/queryKeys.ts); responses are cached and invalidated on mutation rather than blindly re-fetched, and WebSocket pushes feed back into the cache via src/api/websocketBridge.ts.
  • Local UI state stays in the page (useState); shared concerns go through contexts and hooks in src/hooks/ (useTheme, useWebSocket, useClients, useDatepicker, …). Prefer extending an existing hook over introducing a new global.
  • Zod is the single source of truth. Schemas in src/schemas/ define the xray config model; every API response is parsed through them, every form field validates against them, and TypeScript types are inferred with z.infer — never hand-written. Go-side types are mirrored into src/generated/ by npm run gen:zod (do not hand-edit that folder).
  • xray domain logic — link generation, protocol defaults, form ⇄ wire adapters — lives as pure functions in src/lib/xray/. src/models/ keeps only thin legacy types still being migrated onto schemas.
  • HTTP goes through HttpUtil in src/utils/index.ts, a thin fetch wrapper that handles CSRF, response toasts, and a silent: true opt-out for bulk operations that would otherwise spam toasts. The fetch setup itself (base path, CSRF, 401/403 handling) lives in src/api/http-init.ts.

i18n

Locale strings live in internal/web/translation/<locale>.json, not under frontend/. The Go binary embeds the same JSON and serves it to both backend templates and react-i18next (initialized in src/i18n/react.ts). When a new English key is added it must also land in every non-English locale — missing keys do not break the build, they just render the raw key in the UI.

Dev workflows

Goal Command
Iterate on UI changes with HMR cd frontend && npm run dev (Vite on :5173, proxies /panel/* and the WebSocket to the Go panel on :2053). Start the Go panel first.
Verify what end users actually see cd frontend && npm run build, then go run .. The Go binary serves the built bundle — embedded in release mode, off disk in debug mode.
Develop/preview a reusable component in isolation cd frontend && npm run storybook (Storybook workbench + autodocs on :6006).

The Vite dev proxy serves the admin SPA for any /panel/* URL — bypassMigratedRoute in vite.config.js rewrites those requests to index.html and lets React Router take over — while forwarding /panel/api/*, /panel/api/setting/*, /panel/api/xray/*, and the WebSocket to the Go panel. Because routing is now client-side, new panel routes need no proxy or allowlist changes.

XUI_DEBUG=true gotcha — in debug mode the panel serves HTML from the embedded FS (frozen at the last go build / go run) but JS/CSS off disk. Re-running npm run build without restarting Go leaves the embedded HTML pointing at the old hashed asset names, producing a blank page with 404s in the console. Always restart go run . after a frontend rebuild.

Adding a new page

Most new screens are admin-panel routes and need no new HTML or Vite entry:

  1. Create the page component under src/pages/<page>/<Page>.tsx (kebab-case folder, PascalCase component).
  2. Register it in src/routes.tsx under the /panel tree (lazy-import it like the others).
  3. Add a sidebar link in src/layouts/AppSidebar.tsx if it should be reachable from the nav.

Only a genuinely standalone bundle (like login or subpage, reachable without the panel shell) needs the full entry treatment: add frontend/<page>.html, a src/entries/<page>.tsx bootstrap, register it in rollupOptions.input inside vite.config.js, and wire a Go controller route that calls serveDistPage(c, "<page>.html") to serve the embedded HTML in production.

Conventions

  • TypeScript strict mode — all new code in .ts / .tsx. Run npm run typecheck (tsc --noEmit) before pushing. The path alias @/* resolves to src/*.
  • Ant Design 6 is the only UI kit — no Tailwind, no shadcn. A previous attempt to migrate was rolled back. Small, targeted UX tweaks beat sweeping rewrites; raise broader visual changes for discussion before implementing.
  • Function components + hooks everywhere. No class components.
  • Comments in committed Go/TS/TSX: 2 lines MAX per comment block, spent on the why a name cannot hold — an invariant, an issue number, a non-obvious constraint. Names should carry the meaning; rename rather than annotate. Compiler and tool directives (//go:build, //go:generate, //nolint:) are exempt, and HTML <!-- ... --> is fine for template structure.
  • Persian and Arabic users are first-class. When writing Persian text in toasts or labels, isolate code identifiers on their own lines so RTL reading flows. (Full RTL layout is not currently wired through AntD ConfigProvider direction — only the Jalali date picker is RTL-aware — so treat RTL as an open area, not a solved one.)
  • Schemas over any. New config shapes go in src/schemas/; oxlint's typescript/no-explicit-any is an error and production schemas use no .loose(). Validate form fields with antdRule(Schema.shape.field, t) rather than inline z.string() in rules.
  • Document new endpoints. Every new g.POST/g.GET in internal/web/controller/ needs a matching entry in src/pages/api-docs/endpoints.ts — it drives both the in-panel API docs and the generated OpenAPI/Zod (npm run gen:api / gen:zod).
  • Do not break link generation. Share-link logic lives in src/lib/xray/ (inbound-link.ts, outbound-link-parser.ts, …) and is round-tripped by the golden fixture suite — run npm run test after any change to URL generation, defaults, or TLS/Reality handling, and regenerate snapshots (npx vitest run -u) only for intentional changes. Two runtime paths consume it: the inbounds page and the clients page subscription links (/panel/api/clients/subLinks/:subId → backend GetSubs); exercise both.
  • Vite is pinned to an exact version (no ^) in frontend/package.json — read the live version there rather than trusting a number quoted here — so local, CI, and release builds resolve identically. Bump it deliberately and verify both npm run dev and npm run build afterward.
  • Reusable components are documented in Storybook. When you add or change a component in frontend/src/components/, add or update its co-located <Component>.stories.tsx (tags: ['autodocs']), documenting props via argTypes / parameters.docs string metadata rather than JSDoc. CI compile-checks every story via npm run build-storybook and runs each story as a headless-browser test via @storybook/addon-vitest (npm run test, needs npx playwright install chromium); run npm run storybook to preview locally.

Project layout

frontend/
├── index.html             — admin panel SPA entry
├── login.html             — login + 2FA entry
├── subpage.html           — public subscription viewer entry
├── tsconfig.json          — strict, jsx: "react-jsx", paths "@/*" → "src/*"
├── .oxlintrc.json         — oxlint config (typescript + react-hooks + jsx-a11y)
├── tools/oxlint/          — input-number-guard.mjs (#6121/#6127 guard as a JS plugin)
├── vite.config.js
├── vitest.config.ts
├── scripts/               — build-openapi.mjs (endpoints.ts → openapi.json)
└── src/
    ├── main.tsx           — admin SPA bootstrap (router + providers)
    ├── routes.tsx         — react-router routes mounted under /panel
    ├── entries/           — bootstrap for the standalone bundles (login, subpage)
    ├── layouts/           — PanelLayout + AppSidebar
    ├── pages/             — one folder per route (index, inbounds, clients, groups, nodes, settings, xray, api-docs) plus login, sub
    ├── components/        — cross-page React components
    ├── hooks/             — reusable hooks (useTheme, useWebSocket, useClients, useDatepicker, …)
    ├── api/               — fetch client + CSRF handling, TanStack Query provider/keys, WebSocket client
    ├── i18n/              — react-i18next bootstrap (JSON lives in internal/web/translation/)
    ├── lib/xray/          — pure xray logic: link generation, defaults, form ⇄ wire adapters
    ├── schemas/           — Zod source of truth for the xray config model
    ├── generated/         — code-generated Zod + TS types from Go (do not hand-edit)
    ├── models/            — thin legacy types still being migrated
    ├── styles/            — shared CSS (page-cards, …)
    ├── test/              — Vitest specs + golden fixtures
    └── utils/             — HttpUtil, ClipboardManager, SizeFormatter, …

For deeper notes on the frontend toolchain see frontend/README.md.

Project layout

Path Contents
main.go Process entry point, CLI subcommands, signal handling
internal/web/ Gin HTTP server, controllers, services, embedded frontend assets
frontend/ React + Ant Design 6 + TypeScript source for the panel UI
internal/database/ GORM models, migrations, seeders (SQLite / PostgreSQL)
internal/xray/ Xray-core process lifecycle and gRPC API client
internal/sub/ Subscription endpoints (raw, JSON, Clash)
internal/config/ Environment-variable helpers, paths, defaults
x-ui/ Runtime data — db, logs, xray binary, geo files (gitignored)

Testing

Tests live next to the code (foo.gofoo_test.go); frontend specs and golden fixtures live in frontend/src/test/.

Go conventions

  • Stdlib testing only — no testify. Table-driven with t.Run subtests and t.Helper() on helpers.
  • Assert the contract, not internals. Pin the exact value / typed error / emitted string — not err != nil or len > 0. A test that still passes when the behavior is broken is worse than no test.
  • Real dependencies over mocks. Get a throwaway DB with database.InitDB(filepath.Join(t.TempDir(), "x-ui.db")) + t.Cleanup(func() { _ = database.CloseDB() }) (Windows-safe), and use httptest servers for HTTP. The internal/sub suite's initSubDB(t) is the template.

Running

Goal Command
Standard run go test ./...
Hygiene — data races + order-dependence go test -race -shuffle=on -count=1 ./... (-race needs the C compiler from Prerequisites)
Coverage gaps go test -coverprofile=cov.out ./<pkg>/... && go tool cover -func=cov.out
Fuzz a parser briefly go test -run '^$' -fuzz 'FuzzName$' -fuzztime=30s ./<pkg>/...

Frontend: cd frontend && npm run test (vitest), or npm run test -- --coverage.

Property and fuzz tests

Input-heavy or pure logic (link builders, parsers, decoders) is also covered by property tests (pgregory.net/rapid) and native fuzz targets (go test -fuzz). A fuzz target's seed corpus (its inline f.Add cases plus any testdata/fuzz entries) runs as ordinary subtests under a plain go test — no -fuzz flag needed — so CI's normal test job exercises the seeds; the time-boxed fuzzing exploration (-fuzz=...) runs separately as the fuzz-smoke job.

Mutation testing (optional, manual)

gremlins checks whether tests actually fail when the code is mutated — a surviving (LIVED) mutant means a weak test. It is slow, so run it scoped per package, never repo-wide or per-commit:

go install github.com/go-gremlins/gremlins/cmd/gremlins@latest
gremlins unleash ./internal/sub/
gremlins unleash -E 'server\.go|xray\.go|inbound\.go|client_bulk\.go|inbound_traffic\.go|.*_postgres_test\.go' ./internal/web/service/

Treat each survivor as one of: a weak test (strengthen it), dead code (remove it), or an equivalent mutant (unkillable — leave it). Don't write a test purely to kill a mutant if it doesn't reflect real behavior.

CI runs this for you nightly (and on demand) via .github/workflows/mutation.yml — scoped per package, results uploaded as artifacts. It is informational, not a gate (no thresholds), so check the reports when hardening a suite rather than waiting for a red build.

CI

.github/workflows/ci.yml runs per PR: go-test (with -shuffle -count=1), a race job (-race -shuffle -count=1), a fuzz-smoke job on the critical parsers, and the frontend typecheck/lint/format:check/test/build/build-storybook. Snapshots are regression guards — regenerate them (npx vitest run -u) only for intentional output changes, never to make a red test green.

Sending a pull request

  1. Branch off main (e.g. feat/short-description).
  2. Keep the diff focused — separate refactors from feature work.
  3. Run the relevant checks before pushing:
    • go build ./...
    • go test ./... (when Go code changed)
    • cd frontend && npm run typecheck && npm run lint && npm run format:check && npm run test && npm run build && npm run build-storybook (when the frontend changed; CI runs this same set on every PR via .github/workflows/ci.yml)
  4. Commit messages follow the existing pattern in git log<area>: short imperative summary, then a body explaining the why. Conventional-commit prefixes (feat, fix, refactor, chore, style, docs) are encouraged.
  5. Open the PR against main with a brief description of what changed and how to test it.

Useful environment variables

Variable Default Purpose
XUI_DEBUG false Verbose logs + Gin debug mode + serve /assets from disk
XUI_LOG_LEVEL info debug / info / notice / warning / error
XUI_DB_FOLDER platform default Where x-ui.db lives
XUI_LOG_FOLDER platform default Where 3xui.log lives
XUI_BIN_FOLDER bin Where the xray binary, geo files, and xray config.json live
XUI_INIT_WEB_BASE_PATH / The initial URI path for the web panel
XUI_PORT persisted webPort Runtime-only web panel listener port override (1 through 65535)
XUI_DB_TYPE sqlite Set to postgres to use PostgreSQL via XUI_DB_DSN
XUI_DB_DSN PostgreSQL DSN when XUI_DB_TYPE=postgres

A valid XUI_PORT takes precedence over the database-backed webPort for the current process without changing the stored setting. Unset, empty, whitespace-only, malformed, or out-of-range values fall back to webPort; invalid configured values also produce a warning. With Docker bridge networking, the published container port must match the override, for example XUI_PORT: "8080" with ports: ["8080:8080"].

Issues

Before filing a bug, include the OS, Go version, panel version (/panel/api/server/status or the dashboard footer), and the relevant excerpt from x-ui/3xui.log.