From 7da8fd23beecadc789a71b512a82ff94ee60c596 Mon Sep 17 00:00:00 2001 From: Benjamin Mitchell <352241654+BenjaminMitchell-D0FB@users.noreply.github.com> Date: Thu, 12 Nov 2026 14:36:22 -0800 Subject: [PATCH] fix(groups): preserve group traffic totals on global client reset (#7094) Adjust group baselines before clearing client counters so group usage survives a global reset. --- .../web/service/client_group_reset_test.go | 34 +++++++++++++++++++ internal/web/service/client_groups.go | 12 +++++++ internal/web/service/client_traffic.go | 3 ++ 3 files changed, 49 insertions(+) diff --git a/internal/web/service/client_group_reset_test.go b/internal/web/service/client_group_reset_test.go index 5ba06026a..d06b98a68 100644 --- a/internal/web/service/client_group_reset_test.go +++ b/internal/web/service/client_group_reset_test.go @@ -174,6 +174,40 @@ func TestGroupTotalsSurviveBulkClientReset(t *testing.T) { } } +func TestGroupTotalsSurviveGlobalClientReset(t *testing.T) { + initTrafficTestDB(t) + svc := &ClientService{} + + seedGroupedClient(t, "alice", "reset", 100, 200) + seedGroupedClient(t, "bob", "unreset", 40, 60) + if err := svc.ResetGroupTraffic("reset"); err != nil { + t.Fatalf("ResetGroupTraffic: %v", err) + } + + if _, err := svc.ResetAllTraffics(); err != nil { + t.Fatalf("ResetAllTraffics: %v", err) + } + if g := groupByName(t, svc, "unreset"); g.Up != 40 || g.Down != 60 || g.TrafficUsed != 100 { + t.Fatalf("global reset changed accumulated group usage: got %+v, want up=40 down=60 used=100", g) + } + + var traffic xray.ClientTraffic + if err := database.GetDB().Where("email = ?", "alice").First(&traffic).Error; err != nil { + t.Fatalf("load alice traffic: %v", err) + } + if traffic.Up != 0 || traffic.Down != 0 { + t.Fatalf("client traffic not reset: up=%d down=%d", traffic.Up, traffic.Down) + } + + if err := database.GetDB().Model(&traffic). + Updates(map[string]any{"up": 10, "down": 20}).Error; err != nil { + t.Fatalf("record new alice traffic: %v", err) + } + if g := groupByName(t, svc, "reset"); g.Up != 10 || g.Down != 20 || g.TrafficUsed != 30 { + t.Fatalf("global reset hid new group usage: got %+v, want up=10 down=20 used=30", g) + } +} + func TestGroupTotalsSurviveClientDelete(t *testing.T) { initTrafficTestDB(t) csvc := &ClientService{} diff --git a/internal/web/service/client_groups.go b/internal/web/service/client_groups.go index 504b4559a..6db1c448a 100644 --- a/internal/web/service/client_groups.go +++ b/internal/web/service/client_groups.go @@ -67,6 +67,14 @@ func adjustGroupBaselinesForRemovedTraffic(tx *gorm.DB, emails []string) error { return shiftGroupBaselines(tx, emails, -1) } +func adjustAllGroupBaselinesForRemovedTraffic(tx *gorm.DB) error { + totals, err := sumGroupTraffic(tx, "c.group_name <> ''") + if err != nil { + return err + } + return applyGroupBaselineShift(tx, totals, -1) +} + // adjustGroupBaselinesForRestoredTraffic shifts group baselines up by counters an // import restored, so usage from before the import never enters a group total. func adjustGroupBaselinesForRestoredTraffic(tx *gorm.DB, emails []string) error { @@ -92,6 +100,10 @@ func shiftGroupBaselines(tx *gorm.DB, emails []string, sign int64) error { } } } + return applyGroupBaselineShift(tx, totals, sign) +} + +func applyGroupBaselineShift(tx *gorm.DB, totals map[string]*groupCounters, sign int64) error { for name, d := range totals { if d.up == 0 && d.down == 0 { continue diff --git a/internal/web/service/client_traffic.go b/internal/web/service/client_traffic.go index 025c83c39..3ea5b8aad 100644 --- a/internal/web/service/client_traffic.go +++ b/internal/web/service/client_traffic.go @@ -252,6 +252,9 @@ func (s *ClientService) ResetAllTraffics() (bool, error) { if qErr != nil { return qErr } + if err := adjustAllGroupBaselinesForRemovedTraffic(tx); err != nil { + return err + } if affected, qErr = zeroClientTraffics(tx, nil, keepOff); qErr != nil { return qErr }