CI / go-test (push) Canceled after 0s
CI / postgres-durable-first (push) Canceled after 0s
CI / codegen (push) Canceled after 0s
CI / govulncheck (push) Canceled after 0s
CI / race (push) Canceled after 0s
CI / fuzz-smoke (push) Canceled after 0s
CI / golangci (push) Canceled after 0s
CI / frontend (push) Canceled after 0s
CodeQL Advanced / Analyze (go) (push) Canceled after 0s
CodeQL Advanced / Analyze (actions) (push) Canceled after 0s
CodeQL Advanced / Analyze (javascript-typescript) (push) Canceled after 0s
Docs CI / build (push) Canceled after 0s
Docs Deploy (GitHub Pages) / build (push) Canceled after 0s
Release 3X-UI / build (386) (push) Canceled after 0s
Release 3X-UI / build (amd64) (push) Canceled after 0s
Release 3X-UI / build (arm64) (push) Canceled after 0s
Release 3X-UI / build (armv5) (push) Canceled after 0s
Release 3X-UI / build (armv6) (push) Canceled after 0s
Release 3X-UI / build (armv7) (push) Canceled after 0s
Release 3X-UI / build (s390x) (push) Canceled after 0s
Release 3X-UI / Build for Windows (amd64) (push) Canceled after 0s
Deploy Smoke Tests / noninteractive-install (ubuntu-24.04-arm) (push) Canceled after 0s
Deploy Smoke Tests / noninteractive-install (ubuntu-latest) (push) Canceled after 0s
Deploy Smoke Tests / release-tag-install (ubuntu-24.04-arm) (push) Canceled after 0s
Deploy Smoke Tests / release-tag-install (ubuntu-latest) (push) Canceled after 0s
Docs Deploy (GitHub Pages) / deploy (push) Canceled after 0s
Release 3X-UI / Publish rolling dev release (push) Canceled after 0s
4.0 KiB
4.0 KiB
frontend/CLAUDE.md
Frontend agent guide. Full detail: frontend/README.md and the root
CONTRIBUTING.md ("Working on the frontend"). This is the short version.
What this is
React 19 + Ant Design 6 + Vite 8 + TypeScript. The Vite config is
vite.config.js (plain JS). Three bundles, each emitted into
internal/web/dist/ and embedded into the Go binary:
index.html— admin panel SPA (entrysrc/main.tsx; react-router under/panel, lazy routes).login.html— login + 2FA (src/entries/login.tsx).subpage.html— public subscription viewer (src/entries/subpage.tsx). The@import alias maps tosrc/.
Data flow
- Server state via TanStack Query (
src/api/, keys insrc/api/queryKeys.ts); invalidate on mutation. WebSocket pushes feed the cache (src/api/websocketBridge.ts). - Local UI state in the page (
useState); shared concerns viasrc/hooks/. Extend an existing hook before adding a global. - Zod (
src/schemas/) is the single source of truth for the xray config model. Infer types withz.infer. Go-side types are mirrored intosrc/generated/bynpm run gen:zod(go run ./tools/openapigen) — do not hand-edit that folder (every file is markedDO NOT EDIT). - xray domain logic (links, defaults, form<->wire adapters) is pure functions in
src/lib/xray/. HTTP goes throughHttpUtilinsrc/utils/index.ts.
Rules
- Ant Design 6 for components; no Tailwind/shadcn (a migration was rolled back).
Form state runs on React Hook Form (
src/components/form/rhf/), not Ant Design'sFormstore. - Function components + hooks only; no class components.
- Comments in committed TS/TSX: 2 lines MAX per comment block, spent on the why a name cannot hold (same rule as root CLAUDE.md). HTML comments are fine.
- TS strict; oxlint's
typescript/no-explicit-anyis an error. Build forms withuseZodForm+FormFieldfrom@/components/form/rhf(wrap the tree inFormProvider); validate through thezodResolveror per-fieldrules={{ validate: rhfZodValidate(Schema.shape.field) }}— messages are Zod issue keys resolved viat(), never inlinez.string(). AntD<Form>stays only as a layout wrapper. Complex shared config editors (FinalMask / Sniffing / Sockopt) remain AntD-Formislands wrapped as value/onChange adapters insrc/lib/xray/forms/fields/, bound via aController. - New
g.POST/g.GETroute => add it tosrc/pages/api-docs/endpoints.ts, thennpm run gen. - i18n strings live in
internal/web/translation/<locale>.json, NOT underfrontend/, and are shared with the Go backend. A new English key must be added to every locale. Interpolation here uses single braces{var}, not the i18next default{{var}}. - Persian/Arabic (RTL) users are first-class — isolate code identifiers on their own line when writing Persian text in labels/toasts.
- Vite is pinned to an exact version (no
^) — bump deliberately, then verifynpm run devANDnpm run build.
Adding a panel route
src/pages/<page>/<Page>.tsx(kebab folder, PascalCase component).- Register in
src/routes.tsxunder/panel(lazy import). - Add a sidebar link in
src/layouts/AppSidebar.tsxif it needs nav. Only standalone bundles (login/subpage) need a new.html+src/entries/*+rollupOptions.input(invite.config.js) + a Go controller route.
Commands
npm run dev(HMR on :5173, proxies to the Go panel on :2053 — start Go first).npm run typecheck/npm run lint/npm run test/npm run build.npm run gen=gen:zod(Go →src/generated/) +gen:api(build-openapi.mjs→public/openapi.json).npm run storybook(workbench on :6006) /npm run build-storybook(CI compile-checks every story). Reusablesrc/components/get a co-located<Component>.stories.tsxwithtags: ['autodocs']; document props viaargTypes/parameters.docsstring metadata, never JSDoc.- After
npm run build, RESTARTgo run .(see the XUI_DEBUG gotcha in root CLAUDE.md) before checking the panel.