Переглянути джерело

feat(groups): show upload/download breakdown in group traffic

Add per-group up/down to GroupSummary (backend + schema), surface them
as Upload/Download columns in the groups table, and fold upload/download
into the Total traffic summary card. Rename the group "Clients in group"
column to just "Clients" across all locales.
MHSanaei 1 день тому
батько
коміт
1c5cb84492

+ 31 - 3
frontend/src/pages/groups/GroupsPage.tsx

@@ -22,6 +22,8 @@ import {
 } from 'antd';
 import type { MenuProps, TableColumnsType } from 'antd';
 import {
+  ArrowDownOutlined,
+  ArrowUpOutlined,
   ClockCircleOutlined,
   DeleteOutlined,
   EditOutlined,
@@ -165,6 +167,14 @@ export default function GroupsPage() {
     () => groups.reduce((acc, g) => acc + (g.trafficUsed || 0), 0),
     [groups],
   );
+  const totalUpload = useMemo(
+    () => groups.reduce((acc, g) => acc + (g.up || 0), 0),
+    [groups],
+  );
+  const totalDownload = useMemo(
+    () => groups.reduce((acc, g) => acc + (g.down || 0), 0),
+    [groups],
+  );
 
   function openCreate() {
     setCreateName('');
@@ -417,6 +427,20 @@ export default function GroupsPage() {
       width: 180,
       render: (count: number) => <span>{count || 0}</span>,
     },
+    {
+      title: t('pages.groups.upload'),
+      dataIndex: 'up',
+      key: 'up',
+      width: 140,
+      render: (bytes: number) => <span>{SizeFormatter.sizeFormat(bytes || 0)}</span>,
+    },
+    {
+      title: t('pages.groups.download'),
+      dataIndex: 'down',
+      key: 'down',
+      width: 140,
+      render: (bytes: number) => <span>{SizeFormatter.sizeFormat(bytes || 0)}</span>,
+    },
     {
       title: t('pages.groups.trafficUsed'),
       dataIndex: 'trafficUsed',
@@ -456,26 +480,30 @@ export default function GroupsPage() {
                   <Col span={24}>
                     <Card size="small" hoverable className="summary-card">
                       <Row gutter={[16, isMobile ? 16 : 12]}>
-                        <Col xs={12} sm={8} md={6}>
+                        <Col xs={12} sm={8}>
                           <Statistic
                             title={t('pages.groups.totalGroups')}
                             value={String(totalGroups)}
                             prefix={<TagsOutlined />}
                           />
                         </Col>
-                        <Col xs={12} sm={8} md={6}>
+                        <Col xs={12} sm={8}>
                           <Statistic
                             title={t('pages.groups.totalGroupedClients')}
                             value={String(totalClients)}
                             prefix={<TeamOutlined />}
                           />
                         </Col>
-                        <Col xs={12} sm={8} md={6}>
+                        <Col xs={24} sm={8}>
                           <Statistic
                             title={t('pages.groups.totalTraffic')}
                             value={SizeFormatter.sizeFormat(totalTraffic)}
                             prefix={<RetweetOutlined />}
                           />
+                          <Space size={16} style={{ marginTop: 4, color: 'var(--ant-color-text-secondary)', fontSize: 13 }}>
+                            <span><ArrowUpOutlined /> {SizeFormatter.sizeFormat(totalUpload)}</span>
+                            <span><ArrowDownOutlined /> {SizeFormatter.sizeFormat(totalDownload)}</span>
+                          </Space>
                         </Col>
                       </Row>
                     </Card>

+ 2 - 0
frontend/src/schemas/client.ts

@@ -129,6 +129,8 @@ export const GroupSummarySchema = z.object({
   name: z.string(),
   clientCount: z.number(),
   trafficUsed: z.number().nullable().transform((v) => v ?? 0),
+  up: z.number().nullable().transform((v) => v ?? 0),
+  down: z.number().nullable().transform((v) => v ?? 0),
 });
 
 export const GroupSummaryListSchema = z.array(GroupSummarySchema).nullable().transform((v) => v ?? []);

+ 7 - 3
internal/web/service/client_groups.go

@@ -14,6 +14,8 @@ type GroupSummary struct {
 	Name        string `json:"name"`
 	ClientCount int    `json:"clientCount"`
 	TrafficUsed int64  `json:"trafficUsed"`
+	Up          int64  `json:"up"`
+	Down        int64  `json:"down"`
 }
 
 func (s *ClientService) ListGroups() ([]GroupSummary, error) {
@@ -22,7 +24,7 @@ func (s *ClientService) ListGroups() ([]GroupSummary, error) {
 	// never double-counts a client's traffic.
 	var derived []GroupSummary
 	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").
+		Select("c.group_name AS name, COUNT(*) AS client_count, COALESCE(SUM(ct.up + ct.down), 0) AS traffic_used, 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 <> ''").
 		Group("c.group_name").
@@ -36,17 +38,19 @@ func (s *ClientService) ListGroups() ([]GroupSummary, error) {
 	type groupAgg struct {
 		count   int
 		traffic int64
+		up      int64
+		down    int64
 	}
 	merged := make(map[string]groupAgg, len(derived)+len(stored))
 	for _, g := range stored {
 		merged[g.Name] = groupAgg{}
 	}
 	for _, g := range derived {
-		merged[g.Name] = groupAgg{count: g.ClientCount, traffic: g.TrafficUsed}
+		merged[g.Name] = groupAgg{count: g.ClientCount, traffic: g.TrafficUsed, 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})
+		out = append(out, GroupSummary{Name: name, ClientCount: agg.count, TrafficUsed: agg.traffic, Up: agg.up, Down: agg.down})
 	}
 	sort.Slice(out, func(i, j int) bool {
 		return strings.ToLower(out[i].Name) < strings.ToLower(out[j].Name)

+ 3 - 1
internal/web/translation/ar-EG.json

@@ -812,10 +812,12 @@
     "groups": {
       "title": "المجموعات",
       "name": "الاسم",
-      "clientCount": "عملاء في المجموعة",
+      "clientCount": "العملاء",
       "totalGroups": "إجمالي المجموعات",
       "totalGroupedClients": "العملاء بمجموعة",
       "trafficUsed": "حركة المرور المستخدمة",
+      "upload": "رفع",
+      "download": "تنزيل",
       "totalTraffic": "إجمالي حركة المرور",
       "addGroup": "إضافة مجموعة",
       "createSuccess": "تم إنشاء المجموعة «{name}».",

+ 3 - 1
internal/web/translation/en-US.json

@@ -813,10 +813,12 @@
     "groups": {
       "title": "Groups",
       "name": "Name",
-      "clientCount": "Clients in group",
+      "clientCount": "Clients",
       "totalGroups": "Total groups",
       "totalGroupedClients": "Clients with a group",
       "trafficUsed": "Traffic used",
+      "upload": "Upload",
+      "download": "Download",
       "totalTraffic": "Total traffic",
       "addGroup": "Add Group",
       "createSuccess": "Group \"{name}\" created.",

+ 3 - 1
internal/web/translation/es-ES.json

@@ -812,10 +812,12 @@
     "groups": {
       "title": "Grupos",
       "name": "Nombre",
-      "clientCount": "Clientes en el grupo",
+      "clientCount": "Clientes",
       "totalGroups": "Total de grupos",
       "totalGroupedClients": "Clientes con grupo",
       "trafficUsed": "Tráfico usado",
+      "upload": "Subida",
+      "download": "Bajada",
       "totalTraffic": "Tráfico total",
       "addGroup": "Añadir grupo",
       "createSuccess": "Grupo «{name}» creado.",

+ 3 - 1
internal/web/translation/fa-IR.json

@@ -812,10 +812,12 @@
     "groups": {
       "title": "گروه‌ها",
       "name": "نام",
-      "clientCount": "کاربران در گروه",
+      "clientCount": "کاربران",
       "totalGroups": "تعداد گروه‌ها",
       "totalGroupedClients": "کاربران دارای گروه",
       "trafficUsed": "ترافیک مصرف‌شده",
+      "upload": "آپلود",
+      "download": "دانلود",
       "totalTraffic": "مجموع ترافیک",
       "addGroup": "افزودن گروه",
       "createSuccess": "گروه «{name}» ایجاد شد.",

+ 3 - 1
internal/web/translation/id-ID.json

@@ -812,10 +812,12 @@
     "groups": {
       "title": "Grup",
       "name": "Nama",
-      "clientCount": "Klien di grup",
+      "clientCount": "Klien",
       "totalGroups": "Total grup",
       "totalGroupedClients": "Klien dengan grup",
       "trafficUsed": "Trafik terpakai",
+      "upload": "Unggah",
+      "download": "Unduh",
       "totalTraffic": "Total trafik",
       "addGroup": "Tambah grup",
       "createSuccess": "Grup «{name}» dibuat.",

+ 3 - 1
internal/web/translation/ja-JP.json

@@ -812,10 +812,12 @@
     "groups": {
       "title": "グループ",
       "name": "名前",
-      "clientCount": "グループ内のクライアント",
+      "clientCount": "クライアント",
       "totalGroups": "グループ合計",
       "totalGroupedClients": "グループのあるクライアント",
       "trafficUsed": "使用済みトラフィック",
+      "upload": "アップロード",
+      "download": "ダウンロード",
       "totalTraffic": "合計トラフィック",
       "addGroup": "グループ追加",
       "createSuccess": "グループ「{name}」を作成しました。",

+ 3 - 1
internal/web/translation/pt-BR.json

@@ -812,10 +812,12 @@
     "groups": {
       "title": "Grupos",
       "name": "Nome",
-      "clientCount": "Clientes no grupo",
+      "clientCount": "Clientes",
       "totalGroups": "Total de grupos",
       "totalGroupedClients": "Clientes com grupo",
       "trafficUsed": "Tráfego usado",
+      "upload": "Envio",
+      "download": "Recebimento",
       "totalTraffic": "Tráfego total",
       "addGroup": "Adicionar grupo",
       "createSuccess": "Grupo «{name}» criado.",

+ 3 - 1
internal/web/translation/ru-RU.json

@@ -812,10 +812,12 @@
     "groups": {
       "title": "Группы",
       "name": "Имя",
-      "clientCount": "Клиентов в группе",
+      "clientCount": "Клиенты",
       "totalGroups": "Всего групп",
       "totalGroupedClients": "Клиенты с группой",
       "trafficUsed": "Использованный трафик",
+      "upload": "Отправлено",
+      "download": "Получено",
       "totalTraffic": "Общий трафик",
       "addGroup": "Добавить группу",
       "createSuccess": "Группа «{name}» создана.",

+ 3 - 1
internal/web/translation/tr-TR.json

@@ -813,10 +813,12 @@
     "groups": {
       "title": "Gruplar",
       "name": "İsim",
-      "clientCount": "Gruptaki kullanıcılar",
+      "clientCount": "Kullanıcılar",
       "totalGroups": "Toplam grup",
       "totalGroupedClients": "Grubu olan kullanıcılar",
       "trafficUsed": "Kullanılan trafik",
+      "upload": "Yükleme",
+      "download": "İndirme",
       "totalTraffic": "Toplam trafik",
       "addGroup": "Grup ekle",
       "createSuccess": "«{name}» grubu oluşturuldu.",

+ 3 - 1
internal/web/translation/uk-UA.json

@@ -812,10 +812,12 @@
     "groups": {
       "title": "Групи",
       "name": "Назва",
-      "clientCount": "Клієнтів у групі",
+      "clientCount": "Клієнти",
       "totalGroups": "Всього груп",
       "totalGroupedClients": "Клієнти з групою",
       "trafficUsed": "Використаний трафік",
+      "upload": "Вивантаження",
+      "download": "Завантаження",
       "totalTraffic": "Загальний трафік",
       "addGroup": "Додати групу",
       "createSuccess": "Групу «{name}» створено.",

+ 3 - 1
internal/web/translation/vi-VN.json

@@ -812,10 +812,12 @@
     "groups": {
       "title": "Nhóm",
       "name": "Tên",
-      "clientCount": "Client trong nhóm",
+      "clientCount": "Client",
       "totalGroups": "Tổng số nhóm",
       "totalGroupedClients": "Client có nhóm",
       "trafficUsed": "Lưu lượng đã dùng",
+      "upload": "Tải lên",
+      "download": "Tải xuống",
       "totalTraffic": "Tổng lưu lượng",
       "addGroup": "Thêm nhóm",
       "createSuccess": "Đã tạo nhóm «{name}».",

+ 3 - 1
internal/web/translation/zh-CN.json

@@ -812,10 +812,12 @@
     "groups": {
       "title": "分组",
       "name": "名称",
-      "clientCount": "分组中的客户端",
+      "clientCount": "客户端",
       "totalGroups": "分组总数",
       "totalGroupedClients": "有分组的客户端",
       "trafficUsed": "已用流量",
+      "upload": "上传",
+      "download": "下载",
       "totalTraffic": "总流量",
       "addGroup": "添加分组",
       "createSuccess": "已创建分组 “{name}”。",

+ 3 - 1
internal/web/translation/zh-TW.json

@@ -812,10 +812,12 @@
     "groups": {
       "title": "群組",
       "name": "名稱",
-      "clientCount": "群組中的客戶端",
+      "clientCount": "客戶端",
       "totalGroups": "群組總數",
       "totalGroupedClients": "有群組的客戶端",
       "trafficUsed": "已用流量",
+      "upload": "上傳",
+      "download": "下載",
       "totalTraffic": "總流量",
       "addGroup": "新增群組",
       "createSuccess": "已建立群組「{name}」。",