The issue analyst moves verbatim from claude-bot.yml into its own claude-issue-analyst.yml, so claude-bot.yml now holds only the pull-request side: review, @claude mentions and conflict resolution. The review job's briefing was a single 2,600-character quoted string inside claude_args, unreadable and unreviewable. It now lives in .github/claude/review-job.md, assembled at run time with a "This run" section that hands the reviewer the pinned head SHA, the pull request and the exact check-runs command, and reaches the CLI through --append-system-prompt-file. The agent-mode action sets no system-prompt append of its own, so the file flag cannot collide with one. Findings no longer carry the fix: REVIEW.md and the brief both forbid suggestion blocks, patches and replacement snippets, overriding the code-review skill's --comment step, which attaches a committable suggestion to any small fix. A finding states what is wrong, where, what triggers it and what breaks; the maintainer decides the change.
10 KiB
Repository context for the Claude bot
Briefing for the issue analyst in .github/workflows/claude-issue-analyst.yml.
It exists so these facts live in ONE place next to the code instead of being
restated in the prompt, where they went stale silently. (Pull-request review is
separate: the code-review skill in .github/workflows/claude-bot.yml is briefed
with CLAUDE.md, REVIEW.md and .github/claude/review-job.md, not this.)
CLAUDE.md, frontend/CLAUDE.md and docs/architecture.md outrank this file.
Where they disagree with it, they win and this file is the thing to fix.
docs/architecture.md carries a "Symptom -> File" index and the cron-job table,
which answer "which file owns X" in one hop; grepping blind wastes turns on a
question it already answers.
Stack
3x-ui is an open-source web control panel for managing Xray-core servers.
- Backend: Go 1.27, module
github.com/mhsanaei/3x-ui/v3, Gin and GORM. - It runs Xray-core as a managed child process (
internal/xray/process.go) and importsgithub.com/xtls/xray-corefor config types and the gRPC stats/handler/router API. The release the panel BUNDLES is pinned inDockerInit.sh; the version it COMPILES against is pinned ingo.mod, and the two are not always the same. - MTProto inbounds run a SECOND managed child, the
mtg-multibinary (a multi-secret mtg fork, panel-side code ininternal/mtproto/), one process per inbound. Client, ad-tag and quota/expiry edits are hot-applied through the fork's management API (PUT /secrets) so connections survive, with a process restart as the fallback on older binaries. - Storage: SQLite by default (
/etc/x-ui/x-ui.dbon Linux, the executable directory on Windows) or PostgreSQL (XUI_DB_TYPE/XUI_DB_DSN). The SQLite driver is CGo, soCGO_ENABLED=0builds fail. - Frontend: React 19 + Ant Design 6 + Vite 8 + TypeScript in
frontend/, built intointernal/web/dist/(gitignored) and embedded withembed.FS.
Where things live
| area | path |
|---|---|
entry point + x-ui CLI |
main.go |
| env parsing | internal/config/ |
| schema, migrations | internal/database/, internal/database/model/ |
| Xray child process + config | internal/xray/ |
| MTProto inbounds | internal/mtproto/ |
| subscription server | internal/sub/ |
| HTTP handlers | internal/web/controller/ |
| business logic | internal/web/service/ |
cron jobs (schedules in web.go startTask()) |
internal/web/job/ |
| master/sub-node over mTLS | internal/web/runtime/ |
| i18n | internal/web/locale/, internal/web/translation/ |
| UI source | frontend/src/ |
| install / upgrade | install.sh, x-ui.sh, DockerInit.sh |
Hard rules a change must respect
- Dispatch through
runtime.Runtime. Every state-changing inbound or client operation goes through the interface ininternal/web/runtime/, never straight tointernal/xray/api.go. A direct call passes every local test and silently breaks every multi-node deployment; it is invisible in a single-box reading of a diff. - Layering. Controllers are thin — bind, validate, respond — with no GORM
queries, no Xray calls and no business rules.
internal/util/*is leaf-only and must not import service, controller or database.internal/web/dist/andfrontend/src/generated/are generated; a hand-edit is a violation. - Comments in committed Go/TS/TSX: 2 lines MAX per block, spent on the why
a name cannot hold — an invariant, an issue number, a non-obvious constraint.
Exempt, never flag:
//go:build,//go:generate,//nolint:,// Code generated ... DO NOT EDIT.. HTML<!-- -->is fine. - The route contract chain, which breaks in four distinct places:
- a new
g.POST/g.GETininternal/web/controller/needs a matching entry infrontend/src/pages/api-docs/endpoints.ts— pinned BOTH ways byTestRouteRegistryContractininternal/web/routes_contract_test.go, so a renamed or removed route that leaves a stale entry fails too; - generated artefacts must be regenerated with
make gen, or CI'scodegenjob fails on a dirtyfrontend/src/generatedorfrontend/public/openapi.json; - a NEW struct crossing the API boundary must be added to the
StructAllowallowlist intools/openapigen/main.go, or it is SILENTLY dropped from the schemas andfrontend/scripts/build-openapi.mjsthen fails — a guaranteed CI break, not a style nit; - the step NOTHING checks —
frontend/public/openapi.jsonmust be copied todocs/public/openapi.jsonand the MDX regenerated withcd docs && pnpm gen:api, becausedocs-ci.ymlfires only ondocs/**. Step 4 is the one that reaches production wrong.
- a new
- i18n. A new English key goes in EVERY locale JSON in
internal/web/translation/(13 files) AND must be referenced fromfrontend/srcor Go in the SAME change.frontend/src/test/i18n-dead-keys.test.tsfails on a missing locale file and on an orphan key alike. - Migrations. Schema changes are GORM
AutoMigratePLUS hand-written migrations ininternal/database/db.go. There are no migration files and no down-migrations, and everything has to work on SQLite AND PostgreSQL. - Tests. Stdlib
testingonly (no testify), table-driven witht.Runsubtests andt.Helper()on helpers. An assertion must pin the exact value, typed error or emitted string —err != nilandlen(x) > 0are findings, not nits. Prefer real dependencies: a throwaway DB viadatabase.InitDB(filepath.Join(t.TempDir(), "x-ui.db"))witht.Cleanup, andhttptestfor HTTP.internal/sub'sinitSubDB(t)is the template. A test must FAIL without its fix; one that passes either way certifies nothing and then gets cited as proof the fix works.
The three link implementations
Link and subscription generation is implemented three times, independently:
| language | path | what it feeds |
|---|---|---|
| Go | internal/util/link/, internal/sub/ |
what the panel serves |
| TS | frontend/src/lib/xray/ |
what the panel UI shows |
| TS | docs/lib/xray/ |
what the docs site shows |
A change to share-link or subscription output that touches one and not the others is how they drift apart.
Downstream programs that must accept what the panel emits
- XTLS/Xray-core — the Xray config the panel generates, and the VLESS/VMess transport and security fields.
- MetaCubeX/mihomo — consumes the Clash YAML from
internal/sub/. - SagerNet/sing-box — parses the share links the panel emits.
- mhsanaei/mtg-multi — the MTProto sidecar whose TOML (
[secrets],[secret-ad-tags],[secret-limits]) and management API (PUT /secrets,POST /secrets/{name}/reset-quota)internal/mtproto/writes and calls.
What CI runs
.github/workflows/ci.yml, on every pull request touching Go or frontend code.
It is paths-filtered, so a docs-only or workflow-only change produces no run.
| job | what it proves |
|---|---|
go-test |
go test -shuffle=on -count=1 over every package except frontend/node_modules |
race |
the same set under -race -shuffle=on |
postgres-durable-first |
live PostgreSQL 16: the PostgresCommitFailure tests plus TestHostAutoMigrateCreatesColumns_Postgres and TestMigrate_Postgres. Both steps COUNT passes rather than assert on SKIP, so a renamed or deleted test fails the job |
govulncheck |
known vulnerabilities |
golangci |
golangci-lint |
fuzz-smoke |
30s each on FuzzParseLink and FuzzDecodeCertPin |
codegen |
npm run gen then git diff --exit-code on the generated files |
frontend |
MSW worker drift, lint, format:check, typecheck, npm test (Vitest + headless-Chromium Storybook), build, build-storybook, npm audit |
What CI does NOT prove. These test families t.Skip unless an environment
variable is set, and CI sets only the PostgreSQL ones above:
| gate | covers |
|---|---|
XUI_TEST_PG_DSN |
PostgreSQL-specific paths |
XUI_DB_TYPE + XUI_DB_DSN |
dialect-dependent behaviour |
XRAY_E2E_BINARY |
the Xray gRPC end-to-end tests in internal/xray/ |
XUI_SCALE_TEST |
scale tests in internal/sub/, internal/web/job/, internal/web/service/ |
Mutation testing (mutation.yml) runs nightly and never on a pull request, so a
test that cannot fail is invisible to CI. make verify is the local gate.
Support facts reporters get wrong
- Linux install:
bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh) - Install generates a RANDOM username, password and web base path — never
admin/admin. The
x-uimenu on the server shows or resets them. - The installer service environment file is DISTRO-DEPENDENT:
/etc/default/x-ui(Debian/Ubuntu),/etc/conf.d/x-ui(Arch),/etc/sysconfig/x-ui(RHEL/Fedora). Naming the wrong one means the reporter's edit is silently never read by systemd — a common cause of "I set the variable and nothing happened". - Windows is supported. There the database sits next to the executable, not in
/etc— never quote the Linux path to a Windows user. - SQLite to PostgreSQL:
x-ui migrate-db --dsn "postgres://...", then setXUI_DB_TYPE/XUI_DB_DSNin that file andsystemctl restart x-ui. The source SQLite file is left in place. - Docker image
ghcr.io/mhsanaei/3x-ui; PostgreSQL profiledocker compose --profile postgres up -d. Fail2ban IP-limit enforcement needsNET_ADMIN+NET_RAW(compose grants them; a baredocker runmust add--cap-add=NET_ADMIN --cap-add=NET_RAW). - Never state that a
XUI_*variable does not exist without greppinginternal/config/andinternal/tunnelmonitor/first. TheXUI_TUNNEL_HEALTH_*family is the usual answer to "the panel restarts Xray every few minutes". - Security per inbound is none / tls / reality. XTLS is a VLESS flow
(
xtls-rprx-vision), not a security setting — never tell anyone to pick XTLS in the security dropdown. - Never hardcode a version. For "is this already fixed" use
gh release list -L 10,gh search commits, andgit log -S.