fix(nodes): per-category client email lists behind the node count chips
NodeView carried only the category counts, so the nodes page chips could hover to nothing but the category title. GetAll/recountByGuid now emit online/active/disabled/depleted email lists from the same bucketing pass as the counts (statuses gain the client email; the online list is the counted set), and the chips pop the list over with the inbounds page's popover pattern.
This commit is contained in:
@@ -173,6 +173,12 @@ func (s *NodeService) GetAll() ([]*model.Node, error) {
|
||||
depletedByNode := make(map[int]int)
|
||||
disabledByNode := make(map[int]int)
|
||||
activeByNode := make(map[int]int)
|
||||
// Email lists ride along with the counts, appended in the same pass and
|
||||
// under the same dedupe, so a category's hover list always names exactly
|
||||
// the clients its count tallied.
|
||||
depletedEmailsByNode := make(map[int][]string)
|
||||
disabledEmailsByNode := make(map[int][]string)
|
||||
activeEmailsByNode := make(map[int][]string)
|
||||
statuses, _ := s.nodeClientStatuses()
|
||||
seen := make(map[int]map[int]struct{}, len(nodes))
|
||||
for _, st := range statuses {
|
||||
@@ -190,10 +196,19 @@ func (s *NodeService) GetAll() ([]*model.Node, error) {
|
||||
switch {
|
||||
case st.Depleted:
|
||||
depletedByNode[st.NodeID]++
|
||||
if st.Email != "" {
|
||||
depletedEmailsByNode[st.NodeID] = append(depletedEmailsByNode[st.NodeID], st.Email)
|
||||
}
|
||||
case st.Disabled:
|
||||
disabledByNode[st.NodeID]++
|
||||
if st.Email != "" {
|
||||
disabledEmailsByNode[st.NodeID] = append(disabledEmailsByNode[st.NodeID], st.Email)
|
||||
}
|
||||
default:
|
||||
activeByNode[st.NodeID]++
|
||||
if st.Email != "" {
|
||||
activeEmailsByNode[st.NodeID] = append(activeEmailsByNode[st.NodeID], st.Email)
|
||||
}
|
||||
}
|
||||
}
|
||||
onlineByGuid := s.onlineEmailsByGuid()
|
||||
@@ -204,10 +219,15 @@ func (s *NodeService) GetAll() ([]*model.Node, error) {
|
||||
n.DepletedCount = depletedByNode[n.Id]
|
||||
n.DisabledCount = disabledByNode[n.Id]
|
||||
n.ActiveCount = activeByNode[n.Id]
|
||||
n.DepletedEmails = depletedEmailsByNode[n.Id]
|
||||
n.DisabledEmails = disabledEmailsByNode[n.Id]
|
||||
n.ActiveEmails = activeEmailsByNode[n.Id]
|
||||
// Online is attributed to the node that physically hosts the client
|
||||
// (by GUID): a client on a sub-node counts under the sub-node, not
|
||||
// the intermediate node it syncs through (#4983).
|
||||
n.OnlineCount = len(onlineByGuid[effectiveNodeGuid(n, ambiguous)])
|
||||
onlineSet := onlineByGuid[effectiveNodeGuid(n, ambiguous)]
|
||||
n.OnlineCount = len(onlineSet)
|
||||
n.OnlineEmails = sortedEmailSet(onlineSet)
|
||||
}
|
||||
|
||||
return nodes, nil
|
||||
@@ -219,6 +239,7 @@ type nodeClientStatus struct {
|
||||
InboundID int
|
||||
NodeID int
|
||||
ClientID int
|
||||
Email string
|
||||
Depleted bool
|
||||
Disabled bool
|
||||
}
|
||||
@@ -232,19 +253,20 @@ type nodeClientStatus struct {
|
||||
// depleted (expired/exhausted) wins over disabled.
|
||||
func (s *NodeService) nodeClientStatuses() ([]nodeClientStatus, error) {
|
||||
type row struct {
|
||||
InboundID int `gorm:"column:inbound_id"`
|
||||
NodeID int `gorm:"column:node_id"`
|
||||
ClientID int `gorm:"column:client_id"`
|
||||
Enable bool `gorm:"column:enable"`
|
||||
Total int64 `gorm:"column:total"`
|
||||
Up int64 `gorm:"column:up"`
|
||||
Down int64 `gorm:"column:down"`
|
||||
ExpiryTime int64 `gorm:"column:expiry_time"`
|
||||
InboundID int `gorm:"column:inbound_id"`
|
||||
NodeID int `gorm:"column:node_id"`
|
||||
ClientID int `gorm:"column:client_id"`
|
||||
Email string `gorm:"column:email"`
|
||||
Enable bool `gorm:"column:enable"`
|
||||
Total int64 `gorm:"column:total"`
|
||||
Up int64 `gorm:"column:up"`
|
||||
Down int64 `gorm:"column:down"`
|
||||
ExpiryTime int64 `gorm:"column:expiry_time"`
|
||||
}
|
||||
var rows []row
|
||||
if err := database.GetDB().Table("inbounds").
|
||||
Select("inbounds.id AS inbound_id, inbounds.node_id AS node_id, clients.id AS client_id, " +
|
||||
"clients.enable AS enable, ct.total AS total, ct.up AS up, ct.down AS down, ct.expiry_time AS expiry_time").
|
||||
"clients.email AS email, clients.enable AS enable, ct.total AS total, ct.up AS up, ct.down AS down, ct.expiry_time AS expiry_time").
|
||||
Joins("JOIN client_inbounds ON client_inbounds.inbound_id = inbounds.id").
|
||||
Joins("JOIN clients ON clients.id = client_inbounds.client_id").
|
||||
Joins("LEFT JOIN client_traffics ct ON ct.email = clients.email").
|
||||
@@ -255,7 +277,7 @@ func (s *NodeService) nodeClientStatuses() ([]nodeClientStatus, error) {
|
||||
now := time.Now().UnixMilli()
|
||||
out := make([]nodeClientStatus, 0, len(rows))
|
||||
for _, r := range rows {
|
||||
st := nodeClientStatus{InboundID: r.InboundID, NodeID: r.NodeID, ClientID: r.ClientID}
|
||||
st := nodeClientStatus{InboundID: r.InboundID, NodeID: r.NodeID, ClientID: r.ClientID, Email: r.Email}
|
||||
expired := r.ExpiryTime > 0 && r.ExpiryTime <= now
|
||||
exhausted := r.Total > 0 && r.Up+r.Down >= r.Total
|
||||
switch {
|
||||
@@ -283,6 +305,20 @@ func (s *NodeService) onlineEmailsByGuid() map[string]map[string]struct{} {
|
||||
return out
|
||||
}
|
||||
|
||||
// sortedEmailSet flattens an email set into a deterministic list for the
|
||||
// node views' hover popovers. Nil for an empty set.
|
||||
func sortedEmailSet(set map[string]struct{}) []string {
|
||||
if len(set) == 0 {
|
||||
return nil
|
||||
}
|
||||
out := make([]string, 0, len(set))
|
||||
for email := range set {
|
||||
out = append(out, email)
|
||||
}
|
||||
slices.Sort(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// effectiveNodeGuid is a node's stable online/inbound attribution key: its
|
||||
// reported panelGuid, or a master-local synthetic node-id fallback when the node
|
||||
// has no GUID yet (old build) or its GUID is ambiguous. ambiguous comes from
|
||||
|
||||
Reference in New Issue
Block a user