3x-ui/frontend
MHSanaei 60350f93e7
fix(frontend): Phase 2 Inbound form reactivity bugs (B1-B9, consolidated)
A run of resets dropped the per-bug commits 1401d833 / 5b1ae450 /
5bce0dc5 / 4007eec7. Re-landing all fixes against the same files in one
commit to avoid another rebase-style drop.

B1 — Transmission Select / External Proxy + Sockopt switches didn't
react after click. AntD 6.4.3 Form.useWatch on nested paths doesn't
re-fire reliably after `setFieldValue('streamSettings', cleaned)` on
the parent. Bound Transmission via `name={['streamSettings', 'network']}`
and wrapped the two switches in `<Form.Item shouldUpdate>` blocks that
read state via getFieldValue.

B2 — Security regressed from `Radio.Group buttonStyle="solid"` to a
Select dropdown, and disable state didn't refresh because tlsAllowed/
realityAllowed were derived at the top of the component. Restored
Radio.Button group and moved canEnableTls/canEnableReality evaluation
inside the shouldUpdate render prop.

B3 — Advanced tab "All" sub-tab was missing. Added it as the first
item with a new AdvancedAllEditor that round-trips top-level fields +
the three nested slices on edit.

B4 — Advanced tab title/subtitle and per-section help text were gone.
Wrapped the Tabs in the existing `.advanced-shell` / `.advanced-panel`
structure and restored the `.advanced-editor-meta` help under each
sub-tab using existing i18n keys.

B5 — TLS / Reality sub-forms didn't render when selecting tls or
reality on the Security tab. The `{security === 'tls' && ...}` and
`{security === 'reality' && ...}` conditionals used a stale top-level
useWatch value. Wrapped both in <Form.Item shouldUpdate> blocks that
read `security` via getFieldValue.

B6 — Advanced JSON editors stale after Stream/Sniffing changes. The
editors seeded text via lazy useState and AntD Tabs renders all panes
upfront, so the Advanced tab was already mounted with stale data.
Both AdvancedSliceEditor and AdvancedAllEditor now subscribe via
Form.useWatch and re-sync the text buffer when the watched JSON
differs from a lastEmitRef (the serialization at the moment of our
own last accepted write). User typing doesn't trigger re-sync because
setFieldValue updates lastEmitRef too. (A prior attempt added
`destroyOnHidden` to the outer Tabs but broke conditional tab items
when the unmounted Form.Item for `protocol` lost its value —
abandoned in favor of useWatch reactivity.)

B7 — HeaderMapEditor + button did nothing. addRow() appended a blank
{name:'', value:''} row, but commit() filtered it via rowsToMap before
reaching the form, so AntD saw no change and didn't re-render. The
editor now keeps a local rows state so blank rows survive during
editing; only filled rows are emitted to onChange.

B9 — Sniffing destOverride defaults (HTTP/TLS/QUIC/FAKEDNS) were not
pre-checked on a fresh Add Inbound. buildAddModeValues() seeded
sniffing: {} which left destOverride undefined. Now seeds with
SniffingSchema.parse({}) so the Zod defaults populate.
2026-05-26 16:00:42 +02:00
..
public feat(frontend): TanStack Query + React Router migration & in-panel API docs (#4541) 2026-05-24 21:34:52 +02:00
scripts refactor(frontend): port api-docs/endpoints to TypeScript 2026-05-25 15:29:26 +02:00
src fix(frontend): Phase 2 Inbound form reactivity bugs (B1-B9, consolidated) 2026-05-26 16:00:42 +02:00
.gitignore Vue3 migration (#4198) 2026-05-09 17:47:35 +02:00
eslint.config.js chore(frontend): enforce no-explicit-any: error + add typecheck/test to CI 2026-05-26 12:31:01 +02:00
index.html feat(inbound): Advanced XHTTP and external TLS proxy settings (#4491) 2026-05-24 21:54:26 +02:00
login.html Frontend rewrite: React + TypeScript with AntD v6 (#4498) 2026-05-23 15:21:45 +02:00
package-lock.json test(frontend): vitest harness with golden-file fixtures for inbound protocols 2026-05-25 23:22:12 +02:00
package.json test(frontend): vitest harness with golden-file fixtures for inbound protocols 2026-05-25 23:22:12 +02:00
README.md Frontend rewrite: React + TypeScript with AntD v6 (#4498) 2026-05-23 15:21:45 +02:00
subpage.html Frontend rewrite: React + TypeScript with AntD v6 (#4498) 2026-05-23 15:21:45 +02:00
tsconfig.json Frontend rewrite: React + TypeScript with AntD v6 (#4498) 2026-05-23 15:21:45 +02:00
vite.config.js fix(vite): bypass es-toolkit CJS shim for recharts deep imports 2026-05-25 17:33:20 +02:00
vitest.config.ts refactor(frontend): extract genVmessLink to lib/xray/inbound-link.ts 2026-05-26 00:07:36 +02:00
ZOD_MIGRATION_STATUS.md docs(frontend): record FinalMaskForm rewrite + hookup in status doc 2026-05-26 14:39:49 +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").