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": "重新命名",