Files
3x-ui/frontend
MHSanaei 1aef7171e3 feat(frontend): atomic swap InboundFormModal to Pattern A
Deletes the 2261-line class-mutation modal and renames the
1900-line sibling rewrite into its place. InboundsPage.tsx already
imports the file by path so no consumer change is needed — the swap
is one file delete plus one file rename. Build, lint, and 280 tests
stay green.

What the new modal covers end-to-end:
- Basic (enable / remark / nodeId / protocol / listen / port /
  totalGB / trafficReset / expireDate)
- Sniffing (enabled / destOverride / metadataOnly / routeOnly /
  ipsExcluded / domainsExcluded)
- Protocol per DU branch: VLESS (decryption/encryption + buttons),
  Shadowsocks (method/password/network/ivCheck), HTTP + Mixed
  (accounts list + per-protocol toggles), Tunnel (rewrite + portMap +
  followRedirect), TUN (interface/mtu + four primitive lists +
  userLevel/autoInterface), Wireguard (secretKey + derived pubKey +
  peers list with nested allowedIPs)
- Stream per network: TCP base, KCP, WS, gRPC, HTTPUpgrade, XHTTP
  (the 22-field one), plus external-proxy and sockopt extras
- Security: TLS (SNI/cipher/version/uTLS/ALPN/policy switches +
  certificates list with file/inline toggle + ECH controls), Reality
  (every field + the four API-call buttons), none
- Advanced JSON (settings / streamSettings / sniffing live editors
  that round-trip into form state on every valid parse)
- Fallbacks (load on open for VLESS/Trojan TLS-or-Reality TCP hosts;
  save through the secondary endpoint after the main POST succeeds)

Known regressions vs the legacy modal, all reachable via Advanced JSON
until backfilled in follow-up commits:
- Hysteria stream sub-form (masquerade / udpIdleTimeout / version) —
  schema gap; the existing inbound DU has no hysteria stream branch
- FinalMaskForm hookup — the component is still class-shape coupled
- HeaderMapEditor — TCP request/response headers, WS / HTTPUpgrade /
  XHTTP headers, Hysteria masquerade headers all need a shared editor
- TCP HTTP camouflage request/response body (version, method, path
  list, headers, status, reason) — only the on/off toggle is wired
- Fallbacks polish — up/down move, quick-add-all, rederive-from-child,
  the per-row advanced-toggle / proxy-tag chips

No reference to @/models/inbound's Inbound class anywhere in the new
modal — only @/models/dbinbound (out of scope) and
@/models/reality-targets (out of scope). The protocol-capabilities
predicates and the rawInboundToFormValues + formValuesToWirePayload
adapters carry every behavior the class used to provide.
2026-05-26 11:41:10 +02:00
..
2026-05-09 17:47:35 +02:00

3x-ui frontend

React 19 + Ant Design 6 + TypeScript + Vite 8. Multi-page app — one HTML entry per panel route — built into ../web/dist/ and embedded into the Go binary via embed.FS.

Dev

npm install
npm run dev

Vite serves on http://localhost:5173/. API calls and /panel/* routes proxy to the Go panel at http://localhost:2053/, so start the Go panel first (go run main.go) and then Vite.

The proxy auto-rewrites /panel, /panel/settings, /panel/inbounds, /panel/xray to the matching Vite-served HTML in dev mode (see MIGRATED_ROUTES in vite.config.js), so the sidebar's production-style links work without round-tripping through Go.

Production build

npm run build

Outputs to ../web/dist/ (HTML at the root, hashed JS/CSS under assets/). The Go binary embeds this directory at compile time and web/controller/dist.go serves the per-page HTML.

Type check and lint

npm run typecheck
npm run lint

tsc --noEmit against tsconfig.json (strict mode, jsx: "react-jsx", @/*src/* alias). ESLint 10 with eslint.config.js (flat config) — @eslint/js recommended plus typescript-eslint and eslint-plugin-react-hooks rules.

Layout

frontend/
├── *.html                 # Vite entry HTML, one per panel route
├── tsconfig.json
├── eslint.config.js
├── vite.config.js
└── src/
    ├── entries/           # Per-page bootstrap (createRoot + render)
    ├── pages/             # One folder per route, each with the page
    │   ├── index/         # component + helpers + sub-components
    │   ├── login/
    │   ├── inbounds/
    │   ├── clients/
    │   ├── xray/
    │   ├── nodes/
    │   ├── settings/
    │   ├── api-docs/
    │   └── sub/
    ├── components/        # Cross-page React components
    ├── hooks/             # Reusable hooks (useTheme, useWebSocket, …)
    ├── api/               # Axios setup, CSRF interceptor, WebSocket
    ├── i18n/              # react-i18next init (locales live in web/translation/)
    ├── models/            # Inbound, Outbound, Status, … domain classes
    ├── styles/            # Shared CSS modules (page-cards, …)
    └── utils/             # HttpUtil, ObjectUtil, LanguageManager, …

Adding a new page

  1. Add frontend/<page>.html referencing /src/entries/<page>.tsx.
  2. Add src/entries/<page>.tsx that imports the page component and mounts it with createRoot(...).render(...).
  3. Add the page component under src/pages/<page>/.
  4. Register the entry in rollupOptions.input in vite.config.js.
  5. If the page is reachable from the sidebar at /panel/<route>, add it to MIGRATED_ROUTES so the dev proxy serves the Vite HTML.
  6. Wire the Go controller to serveDistPage(c, "<page>.html").