From 1fa51cf0f27991b8e8eeb3c0ab44046c417baf34 Mon Sep 17 00:00:00 2001 From: Sanaei Date: Mon, 8 Jun 2026 23:47:59 +0200 Subject: [PATCH] feat(groups): show used traffic per group in groups table Sum up+down across each group's clients via a LEFT JOIN on email in ListGroups, expose it as trafficUsed on GroupSummary, and render it as a new column plus a "Total traffic" summary card. Drops the unused "Empty groups" card and its translation key. --- frontend/src/pages/groups/GroupsPage.tsx | 18 +++++++++++----- frontend/src/schemas/client.ts | 1 + web/service/client.go | 26 ++++++++++++++++-------- web/translation/ar-EG.json | 3 ++- web/translation/en-US.json | 3 ++- web/translation/es-ES.json | 3 ++- web/translation/fa-IR.json | 3 ++- web/translation/id-ID.json | 3 ++- web/translation/ja-JP.json | 3 ++- web/translation/pt-BR.json | 3 ++- web/translation/ru-RU.json | 3 ++- web/translation/tr-TR.json | 3 ++- web/translation/uk-UA.json | 3 ++- web/translation/vi-VN.json | 3 ++- web/translation/zh-CN.json | 3 ++- web/translation/zh-TW.json | 3 ++- 16 files changed, 57 insertions(+), 27 deletions(-) diff --git a/frontend/src/pages/groups/GroupsPage.tsx b/frontend/src/pages/groups/GroupsPage.tsx index 94b8357fd..45f144c9a 100644 --- a/frontend/src/pages/groups/GroupsPage.tsx +++ b/frontend/src/pages/groups/GroupsPage.tsx @@ -41,7 +41,7 @@ import { useTheme } from '@/hooks/useTheme'; import { useMediaQuery } from '@/hooks/useMediaQuery'; import { usePageTitle } from '@/hooks/usePageTitle'; import { useClients } from '@/hooks/useClients'; -import { HttpUtil } from '@/utils'; +import { HttpUtil, SizeFormatter } from '@/utils'; import { setMessageInstance } from '@/utils/messageBus'; import AppSidebar from '@/layouts/AppSidebar'; import { LazyMount } from '@/components/utility'; @@ -161,8 +161,8 @@ export default function GroupsPage() { () => groups.reduce((acc, g) => acc + (g.clientCount || 0), 0), [groups], ); - const emptyGroups = useMemo( - () => groups.filter((g) => (g.clientCount || 0) === 0).length, + const totalTraffic = useMemo( + () => groups.reduce((acc, g) => acc + (g.trafficUsed || 0), 0), [groups], ); @@ -417,6 +417,13 @@ export default function GroupsPage() { width: 180, render: (count: number) => {count || 0}, }, + { + title: t('pages.groups.trafficUsed'), + dataIndex: 'trafficUsed', + key: 'trafficUsed', + width: 160, + render: (bytes: number) => {SizeFormatter.sizeFormat(bytes || 0)}, + }, ]; const pageClass = useMemo(() => { @@ -465,8 +472,9 @@ export default function GroupsPage() { } /> diff --git a/frontend/src/schemas/client.ts b/frontend/src/schemas/client.ts index 75a06eb42..aa0902ada 100644 --- a/frontend/src/schemas/client.ts +++ b/frontend/src/schemas/client.ts @@ -126,6 +126,7 @@ export const ActiveInboundsByNodeSchema = z export const GroupSummarySchema = z.object({ name: z.string(), clientCount: z.number(), + trafficUsed: z.number().nullable().transform((v) => v ?? 0), }); export const GroupSummaryListSchema = z.array(GroupSummarySchema).nullable().transform((v) => v ?? []); diff --git a/web/service/client.go b/web/service/client.go index 232817b8f..20cb13261 100644 --- a/web/service/client.go +++ b/web/service/client.go @@ -1714,15 +1714,19 @@ func (s *ClientService) ListPaged(inboundSvc *InboundService, settingSvc *Settin type GroupSummary struct { Name string `json:"name"` ClientCount int `json:"clientCount"` + TrafficUsed int64 `json:"trafficUsed"` } func (s *ClientService) ListGroups() ([]GroupSummary, error) { db := database.GetDB() + // email is unique in both clients and client_traffics, so the LEFT JOIN + // never double-counts a client's traffic. var derived []GroupSummary - if err := db.Model(&model.ClientRecord{}). - Select("group_name AS name, COUNT(*) AS client_count"). - Where("group_name <> ''"). - Group("group_name"). + if err := db.Table("clients AS c"). + Select("c.group_name AS name, COUNT(*) AS client_count, COALESCE(SUM(ct.up + ct.down), 0) AS traffic_used"). + Joins("LEFT JOIN client_traffics ct ON ct.email = c.email"). + Where("c.group_name <> ''"). + Group("c.group_name"). Scan(&derived).Error; err != nil { return nil, err } @@ -1730,16 +1734,20 @@ func (s *ClientService) ListGroups() ([]GroupSummary, error) { if err := db.Find(&stored).Error; err != nil { return nil, err } - merged := make(map[string]int, len(derived)+len(stored)) + type groupAgg struct { + count int + traffic int64 + } + merged := make(map[string]groupAgg, len(derived)+len(stored)) for _, g := range stored { - merged[g.Name] = 0 + merged[g.Name] = groupAgg{} } for _, g := range derived { - merged[g.Name] = g.ClientCount + merged[g.Name] = groupAgg{count: g.ClientCount, traffic: g.TrafficUsed} } out := make([]GroupSummary, 0, len(merged)) - for name, count := range merged { - out = append(out, GroupSummary{Name: name, ClientCount: count}) + for name, agg := range merged { + out = append(out, GroupSummary{Name: name, ClientCount: agg.count, TrafficUsed: agg.traffic}) } sort.Slice(out, func(i, j int) bool { return strings.ToLower(out[i].Name) < strings.ToLower(out[j].Name) diff --git a/web/translation/ar-EG.json b/web/translation/ar-EG.json index 6a80f74c5..73889fec4 100644 --- a/web/translation/ar-EG.json +++ b/web/translation/ar-EG.json @@ -812,7 +812,8 @@ "clientCount": "عملاء في المجموعة", "totalGroups": "إجمالي المجموعات", "totalGroupedClients": "العملاء بمجموعة", - "emptyGroups": "مجموعات فارغة", + "trafficUsed": "حركة المرور المستخدمة", + "totalTraffic": "إجمالي حركة المرور", "addGroup": "إضافة مجموعة", "createSuccess": "تم إنشاء المجموعة «{name}».", "rename": "إعادة تسمية", diff --git a/web/translation/en-US.json b/web/translation/en-US.json index 7fef89f32..0d1eddf0b 100644 --- a/web/translation/en-US.json +++ b/web/translation/en-US.json @@ -813,7 +813,8 @@ "clientCount": "Clients in group", "totalGroups": "Total groups", "totalGroupedClients": "Clients with a group", - "emptyGroups": "Empty groups", + "trafficUsed": "Traffic used", + "totalTraffic": "Total traffic", "addGroup": "Add Group", "createSuccess": "Group \"{name}\" created.", "rename": "Rename", diff --git a/web/translation/es-ES.json b/web/translation/es-ES.json index d68d09fa3..b77d78bdc 100644 --- a/web/translation/es-ES.json +++ b/web/translation/es-ES.json @@ -812,7 +812,8 @@ "clientCount": "Clientes en el grupo", "totalGroups": "Total de grupos", "totalGroupedClients": "Clientes con grupo", - "emptyGroups": "Grupos vacíos", + "trafficUsed": "Tráfico usado", + "totalTraffic": "Tráfico total", "addGroup": "Añadir grupo", "createSuccess": "Grupo «{name}» creado.", "rename": "Renombrar", diff --git a/web/translation/fa-IR.json b/web/translation/fa-IR.json index eb879b8f6..ba9de91dd 100644 --- a/web/translation/fa-IR.json +++ b/web/translation/fa-IR.json @@ -812,7 +812,8 @@ "clientCount": "کاربران در گروه", "totalGroups": "تعداد گروه‌ها", "totalGroupedClients": "کاربران دارای گروه", - "emptyGroups": "گروه‌های خالی", + "trafficUsed": "ترافیک مصرف‌شده", + "totalTraffic": "مجموع ترافیک", "addGroup": "افزودن گروه", "createSuccess": "گروه «{name}» ایجاد شد.", "rename": "تغییر نام", diff --git a/web/translation/id-ID.json b/web/translation/id-ID.json index 326040c29..bb3675276 100644 --- a/web/translation/id-ID.json +++ b/web/translation/id-ID.json @@ -812,7 +812,8 @@ "clientCount": "Klien di grup", "totalGroups": "Total grup", "totalGroupedClients": "Klien dengan grup", - "emptyGroups": "Grup kosong", + "trafficUsed": "Trafik terpakai", + "totalTraffic": "Total trafik", "addGroup": "Tambah grup", "createSuccess": "Grup «{name}» dibuat.", "rename": "Ubah nama", diff --git a/web/translation/ja-JP.json b/web/translation/ja-JP.json index 77a47b221..917651405 100644 --- a/web/translation/ja-JP.json +++ b/web/translation/ja-JP.json @@ -812,7 +812,8 @@ "clientCount": "グループ内のクライアント", "totalGroups": "グループ合計", "totalGroupedClients": "グループのあるクライアント", - "emptyGroups": "空のグループ", + "trafficUsed": "使用済みトラフィック", + "totalTraffic": "合計トラフィック", "addGroup": "グループ追加", "createSuccess": "グループ「{name}」を作成しました。", "rename": "名前変更", diff --git a/web/translation/pt-BR.json b/web/translation/pt-BR.json index 128d372ef..de3589d09 100644 --- a/web/translation/pt-BR.json +++ b/web/translation/pt-BR.json @@ -812,7 +812,8 @@ "clientCount": "Clientes no grupo", "totalGroups": "Total de grupos", "totalGroupedClients": "Clientes com grupo", - "emptyGroups": "Grupos vazios", + "trafficUsed": "Tráfego usado", + "totalTraffic": "Tráfego total", "addGroup": "Adicionar grupo", "createSuccess": "Grupo «{name}» criado.", "rename": "Renomear", diff --git a/web/translation/ru-RU.json b/web/translation/ru-RU.json index c6ea26437..b1d8e7254 100644 --- a/web/translation/ru-RU.json +++ b/web/translation/ru-RU.json @@ -812,7 +812,8 @@ "clientCount": "Клиентов в группе", "totalGroups": "Всего групп", "totalGroupedClients": "Клиенты с группой", - "emptyGroups": "Пустые группы", + "trafficUsed": "Использованный трафик", + "totalTraffic": "Общий трафик", "addGroup": "Добавить группу", "createSuccess": "Группа «{name}» создана.", "rename": "Переименовать", diff --git a/web/translation/tr-TR.json b/web/translation/tr-TR.json index 27372908f..0eda26d35 100644 --- a/web/translation/tr-TR.json +++ b/web/translation/tr-TR.json @@ -812,7 +812,8 @@ "clientCount": "Gruptaki kullanıcılar", "totalGroups": "Toplam grup", "totalGroupedClients": "Grubu olan kullanıcılar", - "emptyGroups": "Boş gruplar", + "trafficUsed": "Kullanılan trafik", + "totalTraffic": "Toplam trafik", "addGroup": "Grup ekle", "createSuccess": "«{name}» grubu oluşturuldu.", "rename": "Yeniden adlandır", diff --git a/web/translation/uk-UA.json b/web/translation/uk-UA.json index eab9d9263..e147e423d 100644 --- a/web/translation/uk-UA.json +++ b/web/translation/uk-UA.json @@ -812,7 +812,8 @@ "clientCount": "Клієнтів у групі", "totalGroups": "Всього груп", "totalGroupedClients": "Клієнти з групою", - "emptyGroups": "Порожні групи", + "trafficUsed": "Використаний трафік", + "totalTraffic": "Загальний трафік", "addGroup": "Додати групу", "createSuccess": "Групу «{name}» створено.", "rename": "Перейменувати", diff --git a/web/translation/vi-VN.json b/web/translation/vi-VN.json index 4b3bef426..d66775387 100644 --- a/web/translation/vi-VN.json +++ b/web/translation/vi-VN.json @@ -812,7 +812,8 @@ "clientCount": "Client trong nhóm", "totalGroups": "Tổng số nhóm", "totalGroupedClients": "Client có nhóm", - "emptyGroups": "Nhóm trống", + "trafficUsed": "Lưu lượng đã dùng", + "totalTraffic": "Tổng lưu lượng", "addGroup": "Thêm nhóm", "createSuccess": "Đã tạo nhóm «{name}».", "rename": "Đổi tên", diff --git a/web/translation/zh-CN.json b/web/translation/zh-CN.json index 1eb0940a8..2d0943c24 100644 --- a/web/translation/zh-CN.json +++ b/web/translation/zh-CN.json @@ -812,7 +812,8 @@ "clientCount": "分组中的客户端", "totalGroups": "分组总数", "totalGroupedClients": "有分组的客户端", - "emptyGroups": "空分组", + "trafficUsed": "已用流量", + "totalTraffic": "总流量", "addGroup": "添加分组", "createSuccess": "已创建分组 “{name}”。", "rename": "重命名", diff --git a/web/translation/zh-TW.json b/web/translation/zh-TW.json index 811ac2eb4..cb1cad8b2 100644 --- a/web/translation/zh-TW.json +++ b/web/translation/zh-TW.json @@ -812,7 +812,8 @@ "clientCount": "群組中的客戶端", "totalGroups": "群組總數", "totalGroupedClients": "有群組的客戶端", - "emptyGroups": "空群組", + "trafficUsed": "已用流量", + "totalTraffic": "總流量", "addGroup": "新增群組", "createSuccess": "已建立群組「{name}」。", "rename": "重新命名",