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.
This commit is contained in:
MHSanaei
2026-06-27 16:33:36 +02:00
parent d1c0d77023
commit 9b8a0c9b17
20 changed files with 281 additions and 41 deletions
+6 -9
View File
@@ -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 }));
}
},
});