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>
|
||
|---|---|---|
| .. | ||
| src | ||
| .gitignore | ||
| api-docs.html | ||
| clients.html | ||
| eslint.config.js | ||
| inbounds.html | ||
| index.html | ||
| login.html | ||
| nodes.html | ||
| package-lock.json | ||
| package.json | ||
| README.md | ||
| settings.html | ||
| subpage.html | ||
| vite.config.js | ||
| xray.html | ||
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
- Add
frontend/<page>.htmlreferencing/src/entries/<page>.js. - Add
src/entries/<page>.jsthat imports the page component and mounts it. - Add the page component under
src/pages/<page>/. - Register the entry in
rollupOptions.inputinvite.config.js. - If the page is reachable from the sidebar at
/panel/<route>, add it toMIGRATED_ROUTESso the dev proxy serves the Vite HTML. - Wire the Go controller to
serveDistPage(c, "<page>.html").