feat(panel): customers stats strip with 3x-ui depletion semantics (round-2 #1)
GET /api/customers gains a summary object (total/online/depleted/ depletingSoon/disabled/enabled) computed server-side; the customers page renders it as a six-cell stats strip above the toolbar. Predicates aligned verbatim with 3x-ui client_paging.go: - depleted = (limit>0 && used>=limit) || (expiry set && expiry<=now), counted regardless of status; - depletingSoon = active && !depleted && (expiry within expireDiff days || remaining traffic < trafficDiff GB), thresholds from new settings expireDiff/trafficDiff with 3x-ui's factory default 0 (nobody counts until configured); - enabled = status active; disabled = manual suspensions only. Online reuses the existing 3-minute presence projection. Golden diff confined to the additive summary keys; summary_test.go covers every bucket with thresholds off (soon=0) and on (soon=2).
This commit is contained in:
+53
-1
@@ -551,6 +551,58 @@ func PublicCustomer(item *Customer, data *Data) map[string]any {
|
||||
return out
|
||||
}
|
||||
|
||||
// CustomerSummary computes the customers-page stats strip, mirroring
|
||||
// 3x-ui's client summary semantics (client_paging.go): total / online /
|
||||
// depleted / depletingSoon / disabled / enabled. "depleted" is 3x-ui's
|
||||
// depletedExpr — traffic exhausted (limit > 0 and used >= limit) OR expiry
|
||||
// reached — counted regardless of status. "depletingSoon" is its
|
||||
// expiringExpr — status active, not depleted, and either the expiry is
|
||||
// less than settings.expireDiff days away or the remaining traffic is
|
||||
// less than settings.trafficDiff GB. Both thresholds default to 0 (the
|
||||
// 3x-ui factory default), under which nobody counts as depleting soon.
|
||||
// "disabled" counts manual suspensions only; auto-blocks (expired /
|
||||
// traffic_limit / ip_limit) surface through depleted instead.
|
||||
func CustomerSummary(data *Data, nowMs int64) map[string]any {
|
||||
expireDiffMs := data.Settings.ExpireDiff * 86400000
|
||||
trafficDiffBytes := data.Settings.TrafficDiff * 1073741824
|
||||
total, online, depleted, depletingSoon, disabled, enabled := len(data.Customers), 0, 0, 0, 0, 0
|
||||
for _, c := range data.Customers {
|
||||
if CustomerOnline(c, nowMs) {
|
||||
online++
|
||||
}
|
||||
if c.Status == "active" {
|
||||
enabled++
|
||||
}
|
||||
if c.Status == "suspended" && strVal(c.SuspendReason) == "manual" {
|
||||
disabled++
|
||||
}
|
||||
var expiryMs int64 = -1
|
||||
if c.ExpiresAt != nil {
|
||||
if ms, ok := ParseISOMs(*c.ExpiresAt); ok {
|
||||
expiryMs = ms
|
||||
}
|
||||
}
|
||||
isDepleted := (c.TrafficLimitBytes > 0 && float64(c.UsedBytes) >= c.TrafficLimitBytes) ||
|
||||
(expiryMs >= 0 && expiryMs <= nowMs)
|
||||
if isDepleted {
|
||||
depleted++
|
||||
continue
|
||||
}
|
||||
if c.Status != "active" {
|
||||
continue
|
||||
}
|
||||
nearExpiry := expiryMs >= 0 && float64(expiryMs-nowMs) < expireDiffMs
|
||||
nearLimit := c.TrafficLimitBytes > 0 && c.TrafficLimitBytes-float64(c.UsedBytes) < trafficDiffBytes
|
||||
if nearExpiry || nearLimit {
|
||||
depletingSoon++
|
||||
}
|
||||
}
|
||||
return map[string]any{
|
||||
"total": total, "online": online, "depleted": depleted,
|
||||
"depletingSoon": depletingSoon, "disabled": disabled, "enabled": enabled,
|
||||
}
|
||||
}
|
||||
|
||||
// credentialItems mirrors credentialItems in server.js.
|
||||
func credentialItems(d *Data, customer *Customer) []map[string]any {
|
||||
items := []map[string]any{}
|
||||
@@ -1751,7 +1803,7 @@ func (a *App) handleAdminApi(w http.ResponseWriter, r *http.Request, pathname st
|
||||
out["speedDownBps"] = down
|
||||
customers = append(customers, out)
|
||||
}
|
||||
SendJSON(w, 200, map[string]any{"customers": customers}, nil)
|
||||
SendJSON(w, 200, map[string]any{"customers": customers, "summary": CustomerSummary(data, nowMs)}, nil)
|
||||
return true, nil
|
||||
}
|
||||
if params, ok := RoutePattern("/api/customers/:id/access", pathname); ok && r.Method == http.MethodGet {
|
||||
|
||||
Reference in New Issue
Block a user