From 9b8a0c9b173b9ec227a5a4399a015b4bbc0a5fdc Mon Sep 17 00:00:00 2001 From: MHSanaei Date: Sat, 27 Jun 2026 16:33:36 +0200 Subject: [PATCH] feat(groups): reset group traffic without touching client counters The group page shows traffic counting per group, but the only reset available zeroed every member client's up/down counters (and their quotas) via bulkResetTraffic. Group traffic is a derived sum of client traffic, so zeroing the group display previously required mutating the clients themselves. Add a display-only baseline: ClientGroup gains reset_up/reset_down columns (additive, handled by AutoMigrate). ResetGroupTraffic snapshots the group's current up/down sum into the baseline, and ListGroups now reports max(0, sum - baseline). Client counters are left untouched and no Xray restart is triggered. A new POST /panel/api/clients/groups/ resetTraffic endpoint drives it, creating the client_groups row when the group exists only as a derived label. The groups page action now calls the new endpoint; confirm/success strings updated across all 13 locales to reflect group-only semantics. --- frontend/public/openapi.json | 49 +++++++ frontend/src/pages/api-docs/endpoints.ts | 7 + frontend/src/pages/groups/GroupsPage.tsx | 15 +-- internal/database/model/model.go | 2 + internal/web/controller/group.go | 19 +++ .../web/service/client_group_reset_test.go | 127 ++++++++++++++++++ internal/web/service/client_groups.go | 51 ++++++- internal/web/translation/ar-EG.json | 4 +- internal/web/translation/en-US.json | 4 +- internal/web/translation/es-ES.json | 4 +- internal/web/translation/fa-IR.json | 4 +- internal/web/translation/id-ID.json | 4 +- internal/web/translation/ja-JP.json | 4 +- internal/web/translation/pt-BR.json | 4 +- internal/web/translation/ru-RU.json | 4 +- internal/web/translation/tr-TR.json | 4 +- internal/web/translation/uk-UA.json | 4 +- internal/web/translation/vi-VN.json | 4 +- internal/web/translation/zh-CN.json | 4 +- internal/web/translation/zh-TW.json | 4 +- 20 files changed, 281 insertions(+), 41 deletions(-) create mode 100644 internal/web/service/client_group_reset_test.go diff --git a/frontend/public/openapi.json b/frontend/public/openapi.json index ef0e1fc10..7aab7fcd7 100644 --- a/frontend/public/openapi.json +++ b/frontend/public/openapi.json @@ -6619,6 +6619,55 @@ } } }, + "/panel/api/clients/groups/resetTraffic": { + "post": { + "tags": [ + "Clients" + ], + "summary": "Reset only the group-level traffic counter shown on the groups page. Snapshots the current up/down sum of the group's members as a baseline so the group total reads zero, while leaving each client's own counters (and their quotas) untouched. No Xray restart is triggered. Creates the client_groups row if the group exists only as a derived label.", + "operationId": "post_panel_api_clients_groups_resetTraffic", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object" + }, + "example": { + "name": "customer-a" + } + } + } + }, + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + }, + "msg": { + "type": "string" + }, + "obj": {} + } + }, + "example": { + "success": true, + "obj": { + "name": "customer-a" + } + } + } + } + } + } + } + }, "/panel/api/clients/resetTraffic/{email}": { "post": { "tags": [ diff --git a/frontend/src/pages/api-docs/endpoints.ts b/frontend/src/pages/api-docs/endpoints.ts index bca727ee7..cf6767d7c 100644 --- a/frontend/src/pages/api-docs/endpoints.ts +++ b/frontend/src/pages/api-docs/endpoints.ts @@ -784,6 +784,13 @@ export const sections: readonly Section[] = [ body: '{\n "name": "customer-a"\n}', response: '{\n "success": true,\n "obj": {\n "affected": 5\n }\n}', }, + { + method: 'POST', + path: '/panel/api/clients/groups/resetTraffic', + summary: 'Reset only the group-level traffic counter shown on the groups page. Snapshots the current up/down sum of the group\'s members as a baseline so the group total reads zero, while leaving each client\'s own counters (and their quotas) untouched. No Xray restart is triggered. Creates the client_groups row if the group exists only as a derived label.', + body: '{\n "name": "customer-a"\n}', + response: '{\n "success": true,\n "obj": {\n "name": "customer-a"\n }\n}', + }, { method: 'POST', path: '/panel/api/clients/resetTraffic/:email', diff --git a/frontend/src/pages/groups/GroupsPage.tsx b/frontend/src/pages/groups/GroupsPage.tsx index a64a06f3d..19787c657 100644 --- a/frontend/src/pages/groups/GroupsPage.tsx +++ b/frontend/src/pages/groups/GroupsPage.tsx @@ -126,9 +126,9 @@ export default function GroupsPage() { onSuccess: (msg) => { if (msg?.success) invalidate(); }, }); - const bulkResetMut = useMutation({ - mutationFn: (body: { emails: string[] }) => - HttpUtil.post('/panel/api/clients/bulkResetTraffic', body, JSON_HEADERS), + const groupResetMut = useMutation({ + mutationFn: (body: { name: string }) => + HttpUtil.post('/panel/api/clients/groups/resetTraffic', body, JSON_HEADERS), onSuccess: (msg) => { if (msg?.success) invalidate(); }, }); @@ -321,17 +321,14 @@ export default function GroupsPage() { } modal.confirm({ title: t('pages.groups.resetConfirmTitle', { name: g.name }), - content: t('pages.groups.resetConfirmContent', { count: g.clientCount }), + content: t('pages.groups.resetConfirmContent'), okText: t('reset'), okType: 'danger', cancelText: t('cancel'), onOk: async () => { - const emails = await fetchEmailsForGroup(g.name); - if (emails.length === 0) return; - const msg = await bulkResetMut.mutateAsync({ emails }); + const msg = await groupResetMut.mutateAsync({ name: g.name }); if (msg?.success) { - const affected = (msg.obj as { affected?: number } | undefined)?.affected ?? emails.length; - messageApi.success(t('pages.groups.resetSuccess', { count: affected })); + messageApi.success(t('pages.groups.resetSuccess', { name: g.name })); } }, }); diff --git a/internal/database/model/model.go b/internal/database/model/model.go index a39d3b58a..ffd54ac4c 100644 --- a/internal/database/model/model.go +++ b/internal/database/model/model.go @@ -639,6 +639,8 @@ func (ClientRecord) TableName() string { return "clients" } type ClientGroup struct { Id int `json:"id" gorm:"primaryKey;autoIncrement"` Name string `json:"name" gorm:"uniqueIndex;not null"` + ResetUp int64 `json:"resetUp" gorm:"column:reset_up;default:0"` + ResetDown int64 `json:"resetDown" gorm:"column:reset_down;default:0"` CreatedAt int64 `json:"createdAt" gorm:"autoCreateTime:milli"` UpdatedAt int64 `json:"updatedAt" gorm:"autoUpdateTime:milli"` } diff --git a/internal/web/controller/group.go b/internal/web/controller/group.go index 261c985f4..d7312e3bc 100644 --- a/internal/web/controller/group.go +++ b/internal/web/controller/group.go @@ -26,6 +26,7 @@ func (a *GroupController) initRouter(g *gin.RouterGroup) { g.POST("/groups/create", a.create) g.POST("/groups/rename", a.rename) g.POST("/groups/delete", a.delete) + g.POST("/groups/resetTraffic", a.resetTraffic) g.POST("/groups/bulkAdd", a.bulkAdd) g.POST("/groups/bulkRemove", a.bulkRemove) } @@ -108,6 +109,24 @@ func (a *GroupController) delete(c *gin.Context) { notifyClientsChanged() } +type groupResetTrafficBody struct { + Name string `json:"name"` +} + +func (a *GroupController) resetTraffic(c *gin.Context) { + var body groupResetTrafficBody + if err := c.ShouldBindJSON(&body); err != nil { + jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err) + return + } + if err := a.clientService.ResetGroupTraffic(body.Name); err != nil { + jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err) + return + } + jsonObj(c, gin.H{"name": body.Name}, nil) + notifyClientsChanged() +} + type bulkAddToGroupRequest struct { Emails []string `json:"emails"` Group string `json:"group"` diff --git a/internal/web/service/client_group_reset_test.go b/internal/web/service/client_group_reset_test.go new file mode 100644 index 000000000..c91c9e493 --- /dev/null +++ b/internal/web/service/client_group_reset_test.go @@ -0,0 +1,127 @@ +package service + +import ( + "testing" + + "github.com/mhsanaei/3x-ui/v3/internal/database" + "github.com/mhsanaei/3x-ui/v3/internal/database/model" + "github.com/mhsanaei/3x-ui/v3/internal/xray" +) + +func groupByName(t *testing.T, svc *ClientService, name string) GroupSummary { + t.Helper() + rows, err := svc.ListGroups() + if err != nil { + t.Fatalf("ListGroups: %v", err) + } + for _, g := range rows { + if g.Name == name { + return g + } + } + t.Fatalf("group %q not found in %v", name, rows) + return GroupSummary{} +} + +func seedGroupedClient(t *testing.T, email, group string, up, down int64) { + t.Helper() + if err := database.GetDB().Create(&model.ClientRecord{Email: email, Enable: true, Group: group}).Error; err != nil { + t.Fatalf("seed client record %q: %v", email, err) + } + seedClientRow(t, email, 1, up, down, 0) +} + +func TestResetGroupTraffic_ZeroesGroupButKeepsClients(t *testing.T) { + initTrafficTestDB(t) + svc := &ClientService{} + + seedGroupedClient(t, "alice", "vip", 100, 200) + seedGroupedClient(t, "bob", "vip", 50, 50) + + before := groupByName(t, svc, "vip") + if before.Up != 150 || before.Down != 250 || before.TrafficUsed != 400 || before.ClientCount != 2 { + t.Fatalf("before reset: got %+v, want up=150 down=250 used=400 count=2", before) + } + + if err := svc.ResetGroupTraffic("vip"); err != nil { + t.Fatalf("ResetGroupTraffic: %v", err) + } + + after := groupByName(t, svc, "vip") + if after.Up != 0 || after.Down != 0 || after.TrafficUsed != 0 { + t.Fatalf("after reset: got %+v, want up=0 down=0 used=0", after) + } + if after.ClientCount != 2 { + t.Fatalf("after reset: client count changed to %d, want 2", after.ClientCount) + } + + var alice xray.ClientTraffic + if err := database.GetDB().Where("email = ?", "alice").First(&alice).Error; err != nil { + t.Fatalf("load alice traffic: %v", err) + } + if alice.Up != 100 || alice.Down != 200 { + t.Fatalf("client counter modified by group reset: alice up=%d down=%d, want 100/200", alice.Up, alice.Down) + } +} + +func TestResetGroupTraffic_NewTrafficAccumulatesAboveBaseline(t *testing.T) { + initTrafficTestDB(t) + svc := &ClientService{} + + seedGroupedClient(t, "carol", "team", 100, 100) + if err := svc.ResetGroupTraffic("team"); err != nil { + t.Fatalf("ResetGroupTraffic: %v", err) + } + if g := groupByName(t, svc, "team"); g.Up != 0 || g.Down != 0 { + t.Fatalf("after reset: got %+v, want up=0 down=0", g) + } + + if err := database.GetDB().Table("client_traffics"). + Where("email = ?", "carol"). + Updates(map[string]any{"up": 130, "down": 100}).Error; err != nil { + t.Fatalf("bump carol traffic: %v", err) + } + + g := groupByName(t, svc, "team") + if g.Up != 30 || g.Down != 0 || g.TrafficUsed != 30 { + t.Fatalf("post-bump: got %+v, want up=30 down=0 used=30", g) + } +} + +func TestResetGroupTraffic_CreatesRowForDerivedGroup(t *testing.T) { + initTrafficTestDB(t) + svc := &ClientService{} + + seedGroupedClient(t, "dave", "adhoc", 70, 30) + + var rows int64 + if err := database.GetDB().Model(&model.ClientGroup{}).Where("name = ?", "adhoc").Count(&rows).Error; err != nil { + t.Fatalf("count client_groups: %v", err) + } + if rows != 0 { + t.Fatalf("precondition: derived group should have no client_groups row, got %d", rows) + } + + if err := svc.ResetGroupTraffic("adhoc"); err != nil { + t.Fatalf("ResetGroupTraffic: %v", err) + } + + var stored model.ClientGroup + if err := database.GetDB().Where("name = ?", "adhoc").First(&stored).Error; err != nil { + t.Fatalf("client_groups row not created: %v", err) + } + if stored.ResetUp != 70 || stored.ResetDown != 30 { + t.Fatalf("baseline not snapshotted: got up=%d down=%d, want 70/30", stored.ResetUp, stored.ResetDown) + } + if g := groupByName(t, svc, "adhoc"); g.Up != 0 || g.Down != 0 { + t.Fatalf("after reset: got %+v, want up=0 down=0", g) + } +} + +func TestResetGroupTraffic_EmptyNameRejected(t *testing.T) { + initTrafficTestDB(t) + svc := &ClientService{} + if err := svc.ResetGroupTraffic(" "); err == nil { + t.Fatal("ResetGroupTraffic(blank) = nil, want error") + } +} diff --git a/internal/web/service/client_groups.go b/internal/web/service/client_groups.go index 243b7d98d..63b396945 100644 --- a/internal/web/service/client_groups.go +++ b/internal/web/service/client_groups.go @@ -36,21 +36,32 @@ func (s *ClientService) ListGroups() ([]GroupSummary, error) { return nil, err } type groupAgg struct { - count int - traffic int64 - up int64 - down int64 + count int + up int64 + down int64 } + baseUp := make(map[string]int64, len(stored)) + baseDown := make(map[string]int64, len(stored)) merged := make(map[string]groupAgg, len(derived)+len(stored)) for _, g := range stored { merged[g.Name] = groupAgg{} + baseUp[g.Name] = g.ResetUp + baseDown[g.Name] = g.ResetDown } for _, g := range derived { - merged[g.Name] = groupAgg{count: g.ClientCount, traffic: g.TrafficUsed, up: g.Up, down: g.Down} + merged[g.Name] = groupAgg{count: g.ClientCount, up: g.Up, down: g.Down} } out := make([]GroupSummary, 0, len(merged)) for name, agg := range merged { - out = append(out, GroupSummary{Name: name, ClientCount: agg.count, TrafficUsed: agg.traffic, Up: agg.up, Down: agg.down}) + up := agg.up - baseUp[name] + if up < 0 { + up = 0 + } + down := agg.down - baseDown[name] + if down < 0 { + down = 0 + } + out = append(out, GroupSummary{Name: name, ClientCount: agg.count, TrafficUsed: up + down, Up: up, Down: down}) } sort.Slice(out, func(i, j int) bool { return strings.ToLower(out[i].Name) < strings.ToLower(out[j].Name) @@ -77,6 +88,34 @@ func (s *ClientService) EmailsByGroup(name string) ([]string, error) { return emails, nil } +func (s *ClientService) ResetGroupTraffic(name string) error { + name = strings.TrimSpace(name) + if name == "" { + return common.NewError("group name is required") + } + db := database.GetDB() + var agg struct { + Up int64 + Down int64 + } + if err := db.Table("clients AS c"). + Select("COALESCE(SUM(ct.up), 0) AS up, COALESCE(SUM(ct.down), 0) AS down"). + Joins("LEFT JOIN client_traffics ct ON ct.email = c.email"). + Where("c.group_name = ?", name). + Scan(&agg).Error; err != nil { + return err + } + var count int64 + if err := db.Model(&model.ClientGroup{}).Where("name = ?", name).Count(&count).Error; err != nil { + return err + } + if count == 0 { + return db.Create(&model.ClientGroup{Name: name, ResetUp: agg.Up, ResetDown: agg.Down}).Error + } + return db.Model(&model.ClientGroup{}).Where("name = ?", name). + Updates(map[string]any{"reset_up": agg.Up, "reset_down": agg.Down}).Error +} + func (s *ClientService) CreateGroup(name string) error { name = strings.TrimSpace(name) if name == "" { diff --git a/internal/web/translation/ar-EG.json b/internal/web/translation/ar-EG.json index c56c03307..f2b78d94a 100644 --- a/internal/web/translation/ar-EG.json +++ b/internal/web/translation/ar-EG.json @@ -934,8 +934,8 @@ "deleteSuccess": "تم مسح المجموعة من {count} عميل.", "resetTraffic": "إعادة تعيين حركة المرور", "resetConfirmTitle": "إعادة تعيين حركة المرور للمجموعة {name}؟", - "resetConfirmContent": "يصفر up/down لجميع {count} عميل في هذه المجموعة.", - "resetSuccess": "تمت إعادة تعيين حركة المرور لـ {count} عميل.", + "resetConfirmContent": "يعيد تعيين عداد حركة مرور المجموعة فقط؛ ولا تتأثر عدادات العملاء الفرديين.", + "resetSuccess": "تمت إعادة تعيين حركة مرور المجموعة {name}.", "adjustSuccess": "تم ضبط {count} عميل في {name}.", "emptyForAction": "هذه المجموعة فارغة.", "deleteGroupOnly": "حذف المجموعة (مع الاحتفاظ بالعملاء)", diff --git a/internal/web/translation/en-US.json b/internal/web/translation/en-US.json index c53b0e1e0..f4604dda5 100644 --- a/internal/web/translation/en-US.json +++ b/internal/web/translation/en-US.json @@ -937,8 +937,8 @@ "deleteSuccess": "Cleared group from {count} client(s).", "resetTraffic": "Reset traffic", "resetConfirmTitle": "Reset traffic for group {name}?", - "resetConfirmContent": "This zeros up/down for all {count} client(s) in this group.", - "resetSuccess": "Reset traffic for {count} client(s).", + "resetConfirmContent": "This resets only the group's traffic counter. Individual client counters are not affected.", + "resetSuccess": "Group {name} traffic reset.", "adjustSuccess": "Adjusted {count} client(s) in {name}.", "emptyForAction": "This group has no clients yet.", "deleteGroupOnly": "Delete group (keep clients)", diff --git a/internal/web/translation/es-ES.json b/internal/web/translation/es-ES.json index 0eab8e614..cf41513bd 100644 --- a/internal/web/translation/es-ES.json +++ b/internal/web/translation/es-ES.json @@ -934,8 +934,8 @@ "deleteSuccess": "Grupo limpiado de {count} cliente(s).", "resetTraffic": "Restablecer tráfico", "resetConfirmTitle": "¿Restablecer tráfico del grupo {name}?", - "resetConfirmContent": "Esto pone a cero up/down para los {count} cliente(s) de este grupo.", - "resetSuccess": "Tráfico restablecido en {count} cliente(s).", + "resetConfirmContent": "Esto restablece solo el contador de tráfico del grupo. Los contadores de cada cliente no se ven afectados.", + "resetSuccess": "Tráfico del grupo {name} restablecido.", "adjustSuccess": "Ajustados {count} cliente(s) en {name}.", "emptyForAction": "Este grupo aún no tiene clientes.", "deleteGroupOnly": "Eliminar grupo (conservar clientes)", diff --git a/internal/web/translation/fa-IR.json b/internal/web/translation/fa-IR.json index 4037c92e3..e086311c2 100644 --- a/internal/web/translation/fa-IR.json +++ b/internal/web/translation/fa-IR.json @@ -934,8 +934,8 @@ "deleteSuccess": "گروه از {count} کاربر پاک شد.", "resetTraffic": "بازنشانی ترافیک", "resetConfirmTitle": "بازنشانی ترافیک گروه {name}؟", - "resetConfirmContent": "این عمل آپلود/دانلود تمام {count} کاربر این گروه را صفر می‌کند.", - "resetSuccess": "ترافیک {count} کاربر بازنشانی شد.", + "resetConfirmContent": "این فقط شمارنده‌ی ترافیک گروه را صفر می‌کند؛ شمارنده‌ی تک‌تک کاربران دست‌نخورده می‌ماند.", + "resetSuccess": "ترافیک گروه {name} صفر شد.", "adjustSuccess": "{count} کاربر در {name} تنظیم شد.", "emptyForAction": "این گروه هنوز کاربری ندارد.", "deleteGroupOnly": "حذف گروه (نگه داشتن کاربران)", diff --git a/internal/web/translation/id-ID.json b/internal/web/translation/id-ID.json index 3d0825f8e..cdd5396dc 100644 --- a/internal/web/translation/id-ID.json +++ b/internal/web/translation/id-ID.json @@ -934,8 +934,8 @@ "deleteSuccess": "Grup dihapus dari {count} klien.", "resetTraffic": "Reset trafik", "resetConfirmTitle": "Reset trafik grup {name}?", - "resetConfirmContent": "Ini mengatur ulang up/down ke 0 untuk semua {count} klien di grup ini.", - "resetSuccess": "Trafik direset untuk {count} klien.", + "resetConfirmContent": "Ini hanya mengatur ulang penghitung trafik grup. Penghitung tiap klien tidak terpengaruh.", + "resetSuccess": "Trafik grup {name} direset.", "adjustSuccess": "{count} klien di {name} disesuaikan.", "emptyForAction": "Grup ini belum memiliki klien.", "deleteGroupOnly": "Hapus grup (pertahankan klien)", diff --git a/internal/web/translation/ja-JP.json b/internal/web/translation/ja-JP.json index cc8245fe9..d9160949a 100644 --- a/internal/web/translation/ja-JP.json +++ b/internal/web/translation/ja-JP.json @@ -934,8 +934,8 @@ "deleteSuccess": "{count} クライアントのグループをクリアしました。", "resetTraffic": "トラフィックをリセット", "resetConfirmTitle": "グループ {name} のトラフィックをリセット?", - "resetConfirmContent": "このグループ内のすべての {count} クライアントの up/down をゼロにします。", - "resetSuccess": "{count} クライアントのトラフィックをリセットしました。", + "resetConfirmContent": "グループのトラフィックカウンターのみをリセットします。個々のクライアントのカウンターには影響しません。", + "resetSuccess": "グループ {name} のトラフィックをリセットしました。", "adjustSuccess": "{name} 内の {count} クライアントを調整しました。", "emptyForAction": "このグループにはまだクライアントがありません。", "deleteGroupOnly": "グループ削除 (クライアントは保持)", diff --git a/internal/web/translation/pt-BR.json b/internal/web/translation/pt-BR.json index 5e17c0d21..c4f7a6f4d 100644 --- a/internal/web/translation/pt-BR.json +++ b/internal/web/translation/pt-BR.json @@ -934,8 +934,8 @@ "deleteSuccess": "Grupo limpo de {count} cliente(s).", "resetTraffic": "Redefinir tráfego", "resetConfirmTitle": "Redefinir tráfego do grupo {name}?", - "resetConfirmContent": "Isso zera up/down para todos os {count} cliente(s) deste grupo.", - "resetSuccess": "Tráfego redefinido para {count} cliente(s).", + "resetConfirmContent": "Isso redefine apenas o contador de tráfego do grupo. Os contadores de cada cliente não são afetados.", + "resetSuccess": "Tráfego do grupo {name} redefinido.", "adjustSuccess": "Ajustados {count} cliente(s) em {name}.", "emptyForAction": "Este grupo ainda não tem clientes.", "deleteGroupOnly": "Excluir grupo (manter clientes)", diff --git a/internal/web/translation/ru-RU.json b/internal/web/translation/ru-RU.json index 4d5cc3b65..0d75fb743 100644 --- a/internal/web/translation/ru-RU.json +++ b/internal/web/translation/ru-RU.json @@ -934,8 +934,8 @@ "deleteSuccess": "Группа очищена у {count} клиент(ов).", "resetTraffic": "Сбросить трафик", "resetConfirmTitle": "Сбросить трафик группы {name}?", - "resetConfirmContent": "Это обнулит up/down для всех {count} клиент(ов) в этой группе.", - "resetSuccess": "Сброшен трафик у {count} клиент(ов).", + "resetConfirmContent": "Это сбросит только счётчик трафика группы. Счётчики отдельных клиентов не затрагиваются.", + "resetSuccess": "Трафик группы {name} сброшен.", "adjustSuccess": "Скорректировано {count} клиент(ов) в {name}.", "emptyForAction": "В этой группе пока нет клиентов.", "deleteGroupOnly": "Удалить группу (сохранить клиентов)", diff --git a/internal/web/translation/tr-TR.json b/internal/web/translation/tr-TR.json index e545d9e97..44cbd32e9 100644 --- a/internal/web/translation/tr-TR.json +++ b/internal/web/translation/tr-TR.json @@ -934,8 +934,8 @@ "deleteSuccess": "{count} kullanıcının grubu temizlendi.", "resetTraffic": "Trafiği Sıfırla", "resetConfirmTitle": "{name} Grubunun Trafiğini Sıfırla?", - "resetConfirmContent": "Bu, bu gruptaki tüm {count} kullanıcının yukarı/aşağı trafiğini sıfırlar.", - "resetSuccess": "{count} kullanıcının trafiği sıfırlandı.", + "resetConfirmContent": "Bu yalnızca grubun trafik sayacını sıfırlar. Tek tek kullanıcı sayaçları etkilenmez.", + "resetSuccess": "{name} grubunun trafiği sıfırlandı.", "adjustSuccess": "{name} içinde {count} kullanıcı ayarlandı.", "emptyForAction": "Bu grupta henüz kullanıcı yok.", "deleteGroupOnly": "Grubu Sil (Kullanıcıları Tut)", diff --git a/internal/web/translation/uk-UA.json b/internal/web/translation/uk-UA.json index 6aa934f84..49f755934 100644 --- a/internal/web/translation/uk-UA.json +++ b/internal/web/translation/uk-UA.json @@ -934,8 +934,8 @@ "deleteSuccess": "Групу очищено у {count} клієнт(ів).", "resetTraffic": "Скинути трафік", "resetConfirmTitle": "Скинути трафік групи {name}?", - "resetConfirmContent": "Це обнулить up/down для всіх {count} клієнт(ів) у цій групі.", - "resetSuccess": "Скинуто трафік у {count} клієнт(ів).", + "resetConfirmContent": "Це скине лише лічильник трафіку групи. Лічильники окремих клієнтів не змінюються.", + "resetSuccess": "Трафік групи {name} скинуто.", "adjustSuccess": "Скориговано {count} клієнт(ів) у {name}.", "emptyForAction": "У цій групі ще немає клієнтів.", "deleteGroupOnly": "Видалити групу (зберегти клієнтів)", diff --git a/internal/web/translation/vi-VN.json b/internal/web/translation/vi-VN.json index 91f477a70..f347ab6a7 100644 --- a/internal/web/translation/vi-VN.json +++ b/internal/web/translation/vi-VN.json @@ -934,8 +934,8 @@ "deleteSuccess": "Đã xóa nhóm khỏi {count} client.", "resetTraffic": "Đặt lại lưu lượng", "resetConfirmTitle": "Đặt lại lưu lượng nhóm {name}?", - "resetConfirmContent": "Việc này đưa up/down về 0 cho tất cả {count} client trong nhóm.", - "resetSuccess": "Đã đặt lại lưu lượng cho {count} client.", + "resetConfirmContent": "Việc này chỉ đặt lại bộ đếm lưu lượng của nhóm. Bộ đếm của từng client không bị ảnh hưởng.", + "resetSuccess": "Đã đặt lại lưu lượng nhóm {name}.", "adjustSuccess": "Đã điều chỉnh {count} client trong {name}.", "emptyForAction": "Nhóm này chưa có client.", "deleteGroupOnly": "Xóa nhóm (giữ client)", diff --git a/internal/web/translation/zh-CN.json b/internal/web/translation/zh-CN.json index 6ab1b9adb..1ebe00aa4 100644 --- a/internal/web/translation/zh-CN.json +++ b/internal/web/translation/zh-CN.json @@ -934,8 +934,8 @@ "deleteSuccess": "已清除 {count} 个客户端的分组。", "resetTraffic": "重置流量", "resetConfirmTitle": "重置分组 {name} 的流量?", - "resetConfirmContent": "这将清零此分组中所有 {count} 个客户端的上行/下行流量。", - "resetSuccess": "已重置 {count} 个客户端的流量。", + "resetConfirmContent": "这只会清零该分组的流量计数器,不影响各个客户端的计数器。", + "resetSuccess": "已重置分组 {name} 的流量。", "adjustSuccess": "已调整 {name} 中的 {count} 个客户端。", "emptyForAction": "此分组尚无客户端。", "deleteGroupOnly": "删除分组(保留客户端)", diff --git a/internal/web/translation/zh-TW.json b/internal/web/translation/zh-TW.json index 5bd3e2db5..457753dda 100644 --- a/internal/web/translation/zh-TW.json +++ b/internal/web/translation/zh-TW.json @@ -934,8 +934,8 @@ "deleteSuccess": "已清除 {count} 個客戶端的群組。", "resetTraffic": "重置流量", "resetConfirmTitle": "重置群組 {name} 的流量?", - "resetConfirmContent": "這將將此群組中所有 {count} 個客戶端的上行/下行流量歸零。", - "resetSuccess": "已重置 {count} 個客戶端的流量。", + "resetConfirmContent": "這只會將此群組的流量計數器歸零,不影響各個客戶端的計數器。", + "resetSuccess": "已重置群組 {name} 的流量。", "adjustSuccess": "已調整 {name} 中的 {count} 個客戶端。", "emptyForAction": "此群組尚無客戶端。", "deleteGroupOnly": "刪除群組(保留客戶端)",