mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-09-01 07:57:14 +00:00
fix(ui): classify ended clients as depleted, not disabled, on inbounds page
The auto-disable job flips client.enable off in the settings JSON when a client expires or exhausts its traffic, so the inbounds-page rollup filed every ended client under the gray Disabled badge (and double-counted it in Depleted when stats were present). Classify with depleted-first priority, matching computeClientsSummary and the client info modal. Also backfill cross-inbound client_traffics rows in GetInboundsSlim: the row is keyed on email and only preloads on the inbound the client was created on, so on every other attached inbound the depleted/expiring checks could never fire.
This commit is contained in:
@@ -225,25 +225,35 @@ export function useInbounds() {
|
|||||||
const inboundActive = activeForNode === undefined || !dbInbound.tag || activeForNode.has(dbInbound.tag);
|
const inboundActive = activeForNode === undefined || !dbInbound.tag || activeForNode.has(dbInbound.tag);
|
||||||
|
|
||||||
if (dbInbound.enable) {
|
if (dbInbound.enable) {
|
||||||
|
const statsByEmail = new Map<string, { email: string; total: number; up: number; down: number; expiryTime: number }>();
|
||||||
|
for (const stats of clientStats) {
|
||||||
|
if (stats.email) statsByEmail.set(stats.email.toLowerCase(), stats);
|
||||||
|
}
|
||||||
for (const client of clients) {
|
for (const client of clients) {
|
||||||
if (client.comment && client.email) comments.set(client.email, client.comment);
|
if (client.comment && client.email) comments.set(client.email, client.comment);
|
||||||
if (client.enable) {
|
if (!client.email) continue;
|
||||||
if (client.email) active.push(client.email);
|
const stats = statsByEmail.get(client.email.toLowerCase());
|
||||||
if (client.email && inboundActive && nodeOnline?.has(client.email)) online.push(client.email);
|
const exhausted = stats != null && stats.total > 0 && stats.up + stats.down >= stats.total;
|
||||||
} else if (client.email) {
|
const expired = stats != null && stats.expiryTime > 0 && stats.expiryTime <= now;
|
||||||
deactive.push(client.email);
|
// Depleted wins over disabled (same priority as computeClientsSummary):
|
||||||
}
|
// the auto-disable job also flips client.enable off in settings when a
|
||||||
}
|
// client ends, so checking enable first would file every ended client
|
||||||
for (const stats of clientStats) {
|
// under "Disabled".
|
||||||
const exhausted = stats.total > 0 && stats.up + stats.down >= stats.total;
|
|
||||||
const expired = stats.expiryTime > 0 && stats.expiryTime <= now;
|
|
||||||
if (expired || exhausted) {
|
if (expired || exhausted) {
|
||||||
depleted.push(stats.email);
|
depleted.push(client.email);
|
||||||
} else {
|
continue;
|
||||||
|
}
|
||||||
|
if (!client.enable) {
|
||||||
|
deactive.push(client.email);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
active.push(client.email);
|
||||||
|
if (inboundActive && nodeOnline?.has(client.email)) online.push(client.email);
|
||||||
|
if (stats) {
|
||||||
const expiringSoon =
|
const expiringSoon =
|
||||||
(stats.expiryTime > 0 && stats.expiryTime - now < expireDiffRef.current) ||
|
(stats.expiryTime > 0 && stats.expiryTime - now < expireDiffRef.current) ||
|
||||||
(stats.total > 0 && stats.total - (stats.up + stats.down) < trafficDiffRef.current);
|
(stats.total > 0 && stats.total - (stats.up + stats.down) < trafficDiffRef.current);
|
||||||
if (expiringSoon) expiring.push(stats.email);
|
if (expiringSoon) expiring.push(client.email);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -78,6 +78,10 @@ func (s *InboundService) GetInboundsSlim(userId int) ([]*model.Inbound, error) {
|
|||||||
}
|
}
|
||||||
s.annotateFallbackParents(db, inbounds)
|
s.annotateFallbackParents(db, inbounds)
|
||||||
s.annotateLocalOriginGuid(inbounds)
|
s.annotateLocalOriginGuid(inbounds)
|
||||||
|
// Top up stats rows owned by sibling inbounds (multi-attached clients)
|
||||||
|
// so the list's depleted/expiring badges see every client; the UUID/SubId
|
||||||
|
// enrichment stays skipped. Must run before slimming strips the settings.
|
||||||
|
s.backfillClientStats(db, inbounds)
|
||||||
for _, ib := range inbounds {
|
for _, ib := range inbounds {
|
||||||
ib.Settings = slimSettingsClients(ib.Settings)
|
ib.Settings = slimSettingsClients(ib.Settings)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,31 @@ func (s *InboundService) enrichClientStats(db *gorm.DB, inbounds []*model.Inboun
|
|||||||
if len(inbounds) == 0 {
|
if len(inbounds) == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
clientsByInbound := s.backfillClientStats(db, inbounds)
|
||||||
|
for i, inbound := range inbounds {
|
||||||
|
clients := clientsByInbound[i]
|
||||||
|
if len(clients) == 0 || len(inbound.ClientStats) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
cMap := make(map[string]model.Client, len(clients))
|
||||||
|
for _, c := range clients {
|
||||||
|
cMap[strings.ToLower(c.Email)] = c
|
||||||
|
}
|
||||||
|
for j := range inbound.ClientStats {
|
||||||
|
email := strings.ToLower(inbound.ClientStats[j].Email)
|
||||||
|
if c, ok := cMap[email]; ok {
|
||||||
|
inbound.ClientStats[j].UUID = c.ID
|
||||||
|
inbound.ClientStats[j].SubId = c.SubID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// backfillClientStats tops up each inbound's preloaded ClientStats with rows
|
||||||
|
// owned by a sibling inbound: client_traffics is keyed on email, so a client
|
||||||
|
// attached to several inbounds has one row that only preloads on the inbound
|
||||||
|
// it was created on. Returns the parsed clients per inbound for reuse.
|
||||||
|
func (s *InboundService) backfillClientStats(db *gorm.DB, inbounds []*model.Inbound) [][]model.Client {
|
||||||
clientsByInbound := make([][]model.Client, len(inbounds))
|
clientsByInbound := make([][]model.Client, len(inbounds))
|
||||||
seenByInbound := make([]map[string]struct{}, len(inbounds))
|
seenByInbound := make([]map[string]struct{}, len(inbounds))
|
||||||
missing := make(map[string]struct{})
|
missing := make(map[string]struct{})
|
||||||
@@ -69,7 +94,7 @@ func (s *InboundService) enrichClientStats(db *gorm.DB, inbounds []*model.Inboun
|
|||||||
extra = append(extra, page...)
|
extra = append(extra, page...)
|
||||||
}
|
}
|
||||||
if loadErr != nil {
|
if loadErr != nil {
|
||||||
logger.Warning("enrichClientStats:", loadErr)
|
logger.Warning("backfillClientStats:", loadErr)
|
||||||
} else {
|
} else {
|
||||||
byEmail := make(map[string]xray.ClientTraffic, len(extra))
|
byEmail := make(map[string]xray.ClientTraffic, len(extra))
|
||||||
for _, st := range extra {
|
for _, st := range extra {
|
||||||
@@ -92,23 +117,7 @@ func (s *InboundService) enrichClientStats(db *gorm.DB, inbounds []*model.Inboun
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for i, inbound := range inbounds {
|
return clientsByInbound
|
||||||
clients := clientsByInbound[i]
|
|
||||||
if len(clients) == 0 || len(inbound.ClientStats) == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
cMap := make(map[string]model.Client, len(clients))
|
|
||||||
for _, c := range clients {
|
|
||||||
cMap[strings.ToLower(c.Email)] = c
|
|
||||||
}
|
|
||||||
for j := range inbound.ClientStats {
|
|
||||||
email := strings.ToLower(inbound.ClientStats[j].Email)
|
|
||||||
if c, ok := cMap[email]; ok {
|
|
||||||
inbound.ClientStats[j].UUID = c.ID
|
|
||||||
inbound.ClientStats[j].SubId = c.SubID
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// emailUsedByOtherInbounds reports whether email lives in any inbound other
|
// emailUsedByOtherInbounds reports whether email lives in any inbound other
|
||||||
|
|||||||
Reference in New Issue
Block a user