3x-ui/frontend
MHSanaei 072d266f50
fix(clients): bulk add/delete correctness + perf, working pagination, delayed-start in form
Bulk add/delete were serial on the frontend (one toast per call, N round-trips)
and the backend race exposed by parallelizing them lost client attachments and
hit UNIQUE constraint failed on client_inbounds. The single add/edit modal also
had no Start-After-First-Use option, and the table never showed the delayed
duration.

Backend (web/service/client.go):
- Per-inbound mutex on Add/Update/Del InboundClient so concurrent writers on
  the same inbound don't lose the read-modify-write of settings JSON.
- SyncInbound skips create+join when the email is tombstoned so a concurrent
  maintenance pass (adjustTraffics, autoRenewClients, markClientsDisabledIn-
  Settings) that did a stale RMW can't resurrect a just-deleted client with a
  fresh id.
- compactOrphans sweeps settings.clients entries whose ClientRecord no longer
  exists, applied in Add/DelInboundClient + DelInboundClientByEmail so each
  user-initiated mutation self-heals the inbound's settings.
- DelInboundClient uses Pluck instead of First for the stats lookup so a
  missing row doesn't abort the delete with a noisy ErrRecordNotFound log.

Frontend:
- HttpUtil.{get,post} accept a silent option that suppresses the auto-toast.
- ClientBulkAddModal fires creates in parallel + silent + one summary toast.
- useClients.removeMany runs deletes in parallel + silent and refreshes once;
  ClientsPage bulk delete uses it and shows one aggregate toast.
- useClients.applyInvalidate debounces 200 ms so the burst of N WebSocket
  invalidate events from the backend collapses into a single refresh.
- ClientsPage pagination is reactive (paginationState ref + tablePagination
  computed); onTableChange persists page-size and page changes.
- ClientFormModal gains a Start-After-First-Use switch + Duration days input
  alongside the existing Expiry Date picker; on edit-mode open a negative
  expiryTime is decoded back to delayed mode + days; on submit the payload
  sends -86400000 * days or the absolute timestamp.
- ClientsPage table shows the delayed-start duration (blue tag Nd, tooltip
  Start After First Use: Nd) instead of infinity.
- Telegram ID field in the form is hidden when /panel/setting/defaultSettings
  reports tgBotEnable=false; Comment then fills the row.
- Form row 3 collapses UUID (span 12) + Total GB (span 8) + Limit IP (span 4)
  when ipLimitEnable is on, else UUID + Total GB at 12/12.
- useInbounds.rollupClients counts only clients with a matching clientStats
  row, so orphans in settings.clients no longer inflate the inbound's count.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 22:38:00 +02:00
..
src fix(clients): bulk add/delete correctness + perf, working pagination, delayed-start in form 2026-05-18 22:38:00 +02:00
.gitignore Vue3 migration (#4198) 2026-05-09 17:47:35 +02:00
api-docs.html feat(ui): use the host as the browser tab title prefix 2026-05-13 14:23:57 +02:00
clients.html feat(clients): add top-level Clients tab and CRUD API 2026-05-17 07:28:55 +02:00
eslint.config.js fix: reality random target/sni buttons not working (#4337) (#4340) 2026-05-13 14:42:20 +02:00
inbounds.html feat(ui): use the host as the browser tab title prefix 2026-05-13 14:23:57 +02:00
index.html feat(ui): use the host as the browser tab title prefix 2026-05-13 14:23:57 +02:00
login.html feat(ui): use the host as the browser tab title prefix 2026-05-13 14:23:57 +02:00
nodes.html feat(ui): use the host as the browser tab title prefix 2026-05-13 14:23:57 +02:00
package-lock.json feat(database): add PostgreSQL as an optional backend alongside SQLite 2026-05-18 15:40:39 +02:00
package.json v3.0.2 2026-05-14 10:27:33 +02:00
README.md Vue3 migration (#4198) 2026-05-09 17:47:35 +02:00
settings.html feat(ui): use the host as the browser tab title prefix 2026-05-13 14:23:57 +02:00
subpage.html Vue3 migration (#4198) 2026-05-09 17:47:35 +02:00
vite.config.js fix(inbounds): gate node selector to multi-node-capable protocols 2026-05-18 19:20:10 +02:00
xray.html feat(ui): use the host as the browser tab title prefix 2026-05-13 14:23:57 +02:00

3x-ui frontend

Vue 3 + Ant Design Vue 4 + Vite. 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.

Lint

npm run lint

ESLint 10 with eslint.config.js (flat config) — vue3-recommended plus a few rule overrides for the project's formatting style.

Layout

frontend/
├── *.html                 # Vite entry HTML, one per panel route
├── eslint.config.js
├── vite.config.js
└── src/
    ├── entries/           # Per-page bootstrap (createApp + mount)
    ├── pages/             # One folder per route, each with the page
    │   ├── index/         # component + helpers + sub-components
    │   ├── login/
    │   ├── inbounds/
    │   ├── xray/
    │   ├── settings/
    │   └── sub/
    ├── components/        # Cross-page Vue components
    ├── composables/       # Reusable reactive logic (useTheme, …)
    ├── api/               # Axios setup, CSRF interceptor
    ├── i18n/              # vue-i18n init (locales live in web/translation/)
    ├── models/            # Inbound, Outbound, Status, … domain classes
    └── utils/             # HttpUtil, ObjectUtil, LanguageManager, …

Adding a new page

  1. Add frontend/<page>.html referencing /src/entries/<page>.js.
  2. Add src/entries/<page>.js that imports the page component and mounts it.
  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").