fix(nodes): keep cloned nodes (shared panelGuid) in separate attribution buckets

#4983 keys online/inbound attribution by panelGuid, assuming it is globally unique. Cloned node servers ship an identical panelGuid in their copied settings, so the master collapsed several physical nodes into one bucket: GetMergedNodeTrees merged their online sets under one key and every inbound on those nodes (same origin_node_guid) read that merged set, so the inbound page showed online cross-attributed and counts inflated.

Fall back to the node-unique synthNodeGuid(node.Id) whenever a node's panelGuid is shared by another of the master's direct nodes. Applied consistently at originGuidFor (origin_node_guid write), the online-tree key plus a self-key remap for nodes that report a GUID-keyed tree, effectiveNodeGuid, and recountByGuid's inbound bucketing. sharedNodeGuids computes the collision set. Online now works without node changes; making panelGuids unique restores real-GUID identity and also fixes GUID-keyed IP attribution.
This commit is contained in:
MHSanaei
2026-06-22 14:39:15 +02:00
parent 4854f9c1b8
commit af941798c6
4 changed files with 163 additions and 18 deletions
+36 -8
View File
@@ -192,13 +192,14 @@ func (s *NodeService) GetAll() ([]*model.Node, error) {
}
}
onlineByGuid := s.onlineEmailsByGuid()
shared := sharedNodeGuids(nodes)
for _, n := range nodes {
n.InboundCount = len(inboundsByNode[n.Id])
n.DepletedCount = depletedByNode[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)])
n.OnlineCount = len(onlineByGuid[effectiveNodeGuid(n, shared)])
}
return nodes, nil
@@ -218,14 +219,41 @@ func (s *NodeService) onlineEmailsByGuid() map[string]map[string]struct{} {
return out
}
// effectiveNodeGuid is a node's stable online-attribution key: its reported
// panelGuid, or a master-local synthetic id when the node is an old build that
// hasn't reported one yet (#4983).
func effectiveNodeGuid(n *model.Node) string {
if n.Guid != "" {
return n.Guid
// 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 shares its GUID with another direct node. The
// shared case is a cloned server — the panelGuid is copied with the disk image —
// where an identical GUID would otherwise collapse two physical nodes into one
// #4983 attribution bucket. shared comes from sharedNodeGuids.
func effectiveNodeGuid(n *model.Node, shared map[string]struct{}) string {
if n.Guid == "" {
return synthNodeGuid(n.Id)
}
return synthNodeGuid(n.Id)
if n.Id > 0 {
if _, dup := shared[n.Guid]; dup {
return synthNodeGuid(n.Id)
}
}
return n.Guid
}
// sharedNodeGuids returns the panelGuids reported by more than one of this
// master's own direct nodes (Id > 0). Transitive sub-nodes (Id 0) carry distinct
// descendant GUIDs by construction and are excluded.
func sharedNodeGuids(nodes []*model.Node) map[string]struct{} {
counts := make(map[string]int, len(nodes))
for _, n := range nodes {
if n.Id > 0 && n.Guid != "" {
counts[n.Guid]++
}
}
shared := make(map[string]struct{})
for guid, c := range counts {
if c > 1 {
shared[guid] = struct{}{}
}
}
return shared
}
func (s *NodeService) GetById(id int) (*model.Node, error) {