release: 1.6.0: 30-day send stats and optional auto level-2 rate limits

30-day sending statistics per domain and application, plus optional auto level-2 rate limits derived from average send rate.
Added:
  - Domain page **Sending statistics** card: total messages, peak and average
  - Level-2 rate limit **Manual** / **Auto** mode on domain and application
  - Domain export JSON now includes rate limits (`mode`, `auto_multiplier`,
Changed:
  - Auto mode uses the level-1 window (`RATE_LIMIT_WINDOW_SECONDS`); manual

Full detail: CHANGELOG.md (1.6.0).
This commit is contained in:
mix
2026-09-22 15:24:52 +03:00
parent dca7d99c1b
commit 60b743e6c2
27 changed files with 1092 additions and 140 deletions
@@ -17,6 +17,7 @@ func TestParseDomainRateLimitForm(t *testing.T) {
}
in, err := parseDomainRateLimitForm(form(url.Values{
"mode": {"manual"},
"max_messages": {"50"},
"window_seconds": {"3600"},
}), 100)
@@ -30,6 +31,7 @@ func TestParseDomainRateLimitForm(t *testing.T) {
}
_, err = parseDomainRateLimitForm(form(url.Values{
"mode": {"manual"},
"max_messages": {"150"},
"window_seconds": {"3600"},
}), 100)
@@ -47,6 +49,7 @@ func TestParseAppRateLimitForm(t *testing.T) {
}
in, err := parseAppRateLimitForm(form(url.Values{
"mode": {"manual"},
"allowed_ips": {"203.0.113.10"},
"max_messages": {"80"},
"window_seconds": {"3600"},
@@ -56,6 +59,7 @@ func TestParseAppRateLimitForm(t *testing.T) {
}
_, err = parseAppRateLimitForm(form(url.Values{
"mode": {"manual"},
"max_messages": {"80"},
"window_seconds": {"3600"},
}), 100, 40, true)
@@ -64,6 +68,7 @@ func TestParseAppRateLimitForm(t *testing.T) {
}
_, err = parseAppRateLimitForm(form(url.Values{
"mode": {"manual"},
"allowed_ips": {"203.0.113.10"},
"max_messages": {"40"},
"window_seconds": {"3600"},
@@ -73,6 +78,7 @@ func TestParseAppRateLimitForm(t *testing.T) {
}
_, err = parseAppRateLimitForm(form(url.Values{
"mode": {"manual"},
"allowed_ips": {"203.0.113.10"},
"max_messages": {"150"},
"window_seconds": {"3600"},
@@ -83,6 +89,7 @@ func TestParseAppRateLimitForm(t *testing.T) {
// No domain limit: any app ceiling ≤ L1 is fine.
in, err = parseAppRateLimitForm(form(url.Values{
"mode": {"manual"},
"allowed_ips": {"203.0.113.10"},
"max_messages": {"50"},
"window_seconds": {"3600"},
@@ -91,3 +98,26 @@ func TestParseAppRateLimitForm(t *testing.T) {
t.Fatalf("app without domain = %+v err=%v", in, err)
}
}
func TestParseDomainRateLimitFormAuto(t *testing.T) {
t.Parallel()
form := func(vals url.Values) *http.Request {
r := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(vals.Encode()))
r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
return r
}
in, err := parseDomainRateLimitForm(form(url.Values{
"mode": {"auto"},
"auto_multiplier": {"2.5"},
}), 100)
if err != nil || in.mode != "auto" || in.autoMultiplier != 2.5 {
t.Fatalf("auto domain = %+v err=%v", in, err)
}
_, err = parseDomainRateLimitForm(form(url.Values{
"mode": {"auto"},
"auto_multiplier": {"10"},
}), 100)
if err == nil {
t.Fatal("multiplier out of range should fail")
}
}