diff --git a/docs/Tasktracking/2026-04-26-batch-edit-clients.md b/docs/Tasktracking/2026-04-26-batch-edit-clients.md new file mode 100644 index 00000000..0ff88094 --- /dev/null +++ b/docs/Tasktracking/2026-04-26-batch-edit-clients.md @@ -0,0 +1,60 @@ +Task Record: Batch Edit Clients + +Date: 2026-04-26 +Related Module: Client management (web/service, web/controller, web/html) +Change Type: Add + +Background + +Users previously had to edit each client individually to change shared settings (flow, limit IP, total GB, expiry time, enable state, Telegram ID, comment, reset period). This was time-consuming when managing many clients in an inbound. + +Changes + +Added batch multi-select and batch editing for clients in the inbound expanded view: + +Backend: +- New `BatchUpdateInboundClients` service method in `web/service/inbound.go` - updates multiple clients' common fields in one transaction, syncs client_traffics table (total, expiry_time, enable, reset, tg_id) and Xray API (enable/disable) +- New `BatchUpdateInboundClientsForUser` authorization wrapper +- New `POST /panel/api/inbounds/batchUpdateClients` API route in `web/controller/inbound.go` +- Added `toInt64` helper for JSON number type conversion + +Frontend: +- New `client_batch_edit_modal.html` modal for batch editing common fields (enable, security, flow, limitIP, totalGB, expiryTime, delayedStart, reset, tgId, comment) with "keep unchanged" defaults +- Modified `inbounds.html` expanded row: added row-selection to client table, batch action bar (batch edit, enable/disable, reset traffic, delete, deselect) +- Added `clientSelection` reactive state and helper methods (`getClientRowKey`, `getClientRowSelection`, `getSelectedClients`, `openBatchEditClient`, `batchEnableClient`, `batchResetClientTraffic`, `batchDelClient`, `clearClientSelection`) + +Translations: +- Added keys to `translate.en_US.toml` and `translate.zh_CN.toml`: batchEdit, batchEditAlert, batchKeep, batchEditNoFields, batchDeselect, batchDeleteLastClient, selected + +Fields explicitly NOT batch-editable (enforced both frontend and backend): +- email (unique identifier for traffic tracking) +- id (protocol-specific client UUID) +- subId (subscription identifier) +- password (Trojan client password) + +Impact + +Affected files: +- `web/service/inbound.go` (+~210 lines) +- `web/controller/inbound.go` (+~30 lines) +- `web/html/inbounds.html` (~+90 lines in methods, +15 lines in template) +- `web/html/modals/client_batch_edit_modal.html` (new, 209 lines) +- `web/translation/translate.en_US.toml` (+8 keys) +- `web/translation/translate.zh_CN.toml` (+8 keys) + +No database schema changes. No config changes. No breaking API changes (new route only). + +Verification + +- `go build ./...` - passes +- `CGO_ENABLED=1 go build` - passes +- `go vet ./...` - passes +- `gofmt -d` - no formatting issues + +Not verified: runtime integration testing (requires running panel with Xray and clients). + +Risks And Follow-Up + +- Batch delete does not prevent deleting the LAST client (only warns when trying to delete ALL remaining). The individual delete button already shows/hides based on `isRemovable()`. +- If the Xray API call fails during batch enable/disable, the service returns `needRestart=true` which triggers Xray restart by the periodic check. +- The batch edit modal resets all field values when reopened; no persistence across invocations. diff --git a/web/controller/inbound.go b/web/controller/inbound.go index ea42ffde..c64cd309 100644 --- a/web/controller/inbound.go +++ b/web/controller/inbound.go @@ -55,6 +55,7 @@ func (a *InboundController) initRouter(g *gin.RouterGroup) { g.POST("/lastOnline", a.lastOnline) g.POST("/updateClientTraffic/:email", a.updateClientTraffic) g.POST("/:id/delClientByEmail/:email", a.delInboundClientByEmail) + g.POST("/batchUpdateClients", a.batchUpdateInboundClients) } // getInbounds retrieves the list of inbounds for the logged-in user. @@ -470,6 +471,32 @@ func (a *InboundController) delInboundClientByEmail(c *gin.Context) { } } +// batchUpdateInboundClients updates multiple clients in an inbound with the same field changes. +func (a *InboundController) batchUpdateInboundClients(c *gin.Context) { + var request struct { + InboundID int `json:"inboundId"` + ClientIDs []string `json:"clientIds"` + UpdateFields string `json:"updateFields"` + } + if err := c.ShouldBindJSON(&request); err != nil { + jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.inboundUpdateSuccess"), err) + return + } + + user := session.GetLoginUser(c) + needRestart, err := a.inboundService.BatchUpdateInboundClientsForUser( + user.Id, user.Role == "admin", request.InboundID, request.ClientIDs, request.UpdateFields, + ) + if err != nil { + jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err) + return + } + jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.inboundClientUpdateSuccess"), nil) + if needRestart { + a.xrayService.SetToNeedRestart() + } +} + // getUserInfo returns client traffic information for the logged-in user. func (a *InboundController) getUserInfo(c *gin.Context) { user := session.GetLoginUser(c) diff --git a/web/html/inbounds.html b/web/html/inbounds.html index c8f7363f..971446f7 100644 --- a/web/html/inbounds.html +++ b/web/html/inbounds.html @@ -584,8 +584,21 @@