package httpapi import ( "net/netip" "strings" "time" "evobgp/internal/birdfmt" "evobgp/internal/store" ) type speakerBGPLive struct { SpeakerID string Label string Sessions []birdfmt.BGPSession Error string Polled bool ObservedAt time.Time ReceivedAt time.Time LastSuccessAt time.Time Source string ObservationOverride *liveObservation } type liveObservation struct { ObservedAt *string `json:"observed_at"` ReceivedAt *string `json:"received_at"` LastSuccessAt *string `json:"last_success_at"` ExpiresAt *string `json:"expires_at,omitempty"` Source string `json:"source"` Quality string `json:"quality"` ErrorCode string `json:"error_code,omitempty"` } func liveTime(t time.Time) *string { if t.IsZero() { return nil } v := t.UTC().Format(time.RFC3339Nano) return &v } func (v speakerBGPLive) observed() bool { return v.Polled || v.Error != "" || len(v.Sessions) > 0 } func (v speakerBGPLive) successful() bool { return v.observed() && v.Error == "" } func (v speakerBGPLive) observation() liveObservation { if v.ObservationOverride != nil { return *v.ObservationOverride } o := liveObservation{ObservedAt: liveTime(v.ObservedAt), ReceivedAt: liveTime(v.ReceivedAt), LastSuccessAt: liveTime(v.LastSuccessAt), Source: v.Source, Quality: "unknown"} if o.Source == "" { o.Source = "none" } switch { case v.successful(): o.Quality = "ok" case v.Error != "" && !v.LastSuccessAt.IsZero(): o.Quality = "stale" o.ErrorCode = liveErrorCode(v.Error) case v.Error != "": o.Quality = "unreachable" o.ErrorCode = liveErrorCode(v.Error) default: o.ErrorCode = "not_polled" } return o } func liveErrorCode(err string) string { if strings.Contains(strings.ToLower(err), "not configured") || strings.Contains(strings.ToLower(err), "missing") { return "not_configured" } return "poll_failed" } type peerSessionOnSpeaker struct { SpeakerID string `json:"speaker_id"` Label string `json:"label"` State string `json:"state"` PollError string `json:"poll_error,omitempty"` Observation liveObservation `json:"observation"` } type liveSpeakerPoll struct { SpeakerID string `json:"speaker_id"` Label string `json:"label"` OK bool `json:"ok"` SessionCount *int `json:"session_count"` PollError string `json:"poll_error,omitempty"` Observation liveObservation `json:"observation"` } func speakerDisplayLabel(sp *store.Speaker) string { if sp == nil { return "" } meta := store.ParseSpeakerMeta(sp.MetaJSON) host := strings.TrimSpace(meta.AgentDomain) if host == "" { host = strings.TrimSpace(sp.Endpoint) } if strings.EqualFold(strings.TrimSpace(sp.Role), "master") { if host != "" { return "CP ยท " + host } return "CP (master)" } if host != "" { return host } return sp.ID } func liveSpeakerPollJSON(views []speakerBGPLive) []liveSpeakerPoll { out := make([]liveSpeakerPoll, 0, len(views)) for _, v := range views { var sessionCount *int if v.successful() { count := len(v.Sessions) sessionCount = &count } out = append(out, liveSpeakerPoll{ SpeakerID: v.SpeakerID, Label: v.Label, OK: v.successful(), SessionCount: sessionCount, PollError: v.Error, Observation: v.observation(), }) } return out } func findPeerSession(sessions []birdfmt.BGPSession, protoName string, neighbor netip.Addr, hasNeighbor bool) *birdfmt.BGPSession { for i := range sessions { if peerSessionMatches(sessions[i], protoName, neighbor, hasNeighbor) { return &sessions[i] } } return nil } func matchPeerOnSpeakers(peer *store.BGPPeer, views []speakerBGPLive) ( bestState string, connectedID string, connectedLabel string, establishedOn []peerSessionOnSpeaker, on []peerSessionOnSpeaker, mismatch bool, ) { if peer == nil { return "", "", "", nil, nil, false } neighbor, hasNeighbor := store.ParsePeerNeighbor(peer.Neighbor) protoName := birdfmt.PeerProtocolName(peer.ID) for _, v := range views { if !v.successful() { on = append(on, peerSessionOnSpeaker{ SpeakerID: v.SpeakerID, Label: v.Label, PollError: v.Error, Observation: v.observation(), }) continue } sess := findPeerSession(v.Sessions, protoName, neighbor, hasNeighbor) if sess == nil { on = append(on, peerSessionOnSpeaker{ SpeakerID: v.SpeakerID, Label: v.Label, State: "absent", Observation: v.observation(), }) continue } hit := peerSessionOnSpeaker{ SpeakerID: v.SpeakerID, Label: v.Label, State: sess.State, Observation: v.observation(), } on = append(on, hit) if strings.EqualFold(strings.TrimSpace(sess.State), "Established") { establishedOn = append(establishedOn, hit) } if bestState == "" || sessionStateRank(sess.State) > sessionStateRank(bestState) { bestState = sess.State } } if len(establishedOn) > 0 { bestState = "Established" labels := make([]string, 0, len(establishedOn)) for _, e := range establishedOn { labels = append(labels, e.Label) } connectedLabel = strings.Join(labels, ", ") if len(establishedOn) == 1 { connectedID = establishedOn[0].SpeakerID } } else if len(on) == 1 && on[0].PollError == "" && on[0].State != "" { connectedID = on[0].SpeakerID connectedLabel = on[0].Label } if peer.SpeakerID != nil && strings.TrimSpace(*peer.SpeakerID) != "" && len(establishedOn) > 0 { want := strings.TrimSpace(*peer.SpeakerID) found := false for _, e := range establishedOn { if strings.EqualFold(strings.TrimSpace(e.SpeakerID), want) { found = true break } } mismatch = !found } return bestState, connectedID, connectedLabel, establishedOn, on, mismatch } func peerSessionMatches(sess birdfmt.BGPSession, protoName string, neighbor netip.Addr, hasNeighbor bool) bool { if strings.EqualFold(strings.TrimSpace(sess.Name), protoName) { return true } if !hasNeighbor || strings.TrimSpace(sess.Neighbor) == "" { return false } peerAddr, ok := store.ParsePeerNeighbor(sess.Neighbor) if !ok { return false } return peerAddr == neighbor } func sessionStateRank(state string) int { switch strings.ToLower(strings.TrimSpace(state)) { case "established": return 100 case "openconfirm", "opensent": return 80 case "active", "connect": return 60 case "idle": return 20 default: return 10 } } func applyPeerLiveFields(row map[string]any, peer *store.BGPPeer, views []speakerBGPLive) { expected := expectedPeerViews(peer, views) state, _, _, _, _, _ := matchPeerOnSpeakers(peer, expected) _, connID, connLabel, establishedOn, on, mismatch := matchPeerOnSpeakers(peer, views) row["session_on_speakers"] = on row["established_on_speakers"] = establishedOn row["session_conflict"] = false row["session_mismatch"] = mismatch row["last_known_session_state"] = row["session_state"] row["session_state"] = nil if state != "" { row["session_state"] = state } if state == "" && len(expected) > 0 && allSpeakerPollsSuccessful(expected) { row["session_state"] = "absent" } row["live_observation"] = peerLiveObservation(expected) if connLabel != "" { row["connected_speaker_label"] = connLabel } row["connected_speaker_id"] = peerLiveSpeakerIDOrNull(connID) } func expectedPeerViews(peer *store.BGPPeer, views []speakerBGPLive) []speakerBGPLive { if peer == nil || peer.SpeakerID == nil || strings.TrimSpace(*peer.SpeakerID) == "" { return views } want := strings.TrimSpace(*peer.SpeakerID) expected := make([]speakerBGPLive, 0, 1) for _, v := range views { if strings.EqualFold(v.SpeakerID, want) { expected = append(expected, v) } } return expected } func allSpeakerPollsSuccessful(views []speakerBGPLive) bool { for _, v := range views { if !v.successful() { return false } } return true } func peerLiveObservation(views []speakerBGPLive) liveObservation { if len(views) == 0 { return speakerBGPLive{}.observation() } result := liveObservation{Source: views[0].Source, Quality: "unknown"} if result.Source == "" { result.Source = "none" } var latestObserved, latestReceived, latestSuccess time.Time allOK, allFailed, anyStale := true, true, false for _, v := range views { o := v.observation() if v.ObservedAt.After(latestObserved) { latestObserved = v.ObservedAt } if v.ReceivedAt.After(latestReceived) { latestReceived = v.ReceivedAt } if v.LastSuccessAt.After(latestSuccess) { latestSuccess = v.LastSuccessAt } allOK = allOK && v.successful() allFailed = allFailed && o.Quality == "unreachable" anyStale = anyStale || o.Quality == "stale" if result.ErrorCode == "" && o.ErrorCode != "" { result.ErrorCode = o.ErrorCode } if o.ExpiresAt != nil { expires, err := time.Parse(time.RFC3339Nano, *o.ExpiresAt) if err == nil { previous := time.Time{} if result.ExpiresAt != nil { previous, _ = time.Parse(time.RFC3339Nano, *result.ExpiresAt) } if previous.IsZero() || expires.Before(previous) { result.ExpiresAt = o.ExpiresAt } } } } result.ObservedAt = liveTime(latestObserved) result.ReceivedAt = liveTime(latestReceived) result.LastSuccessAt = liveTime(latestSuccess) if len(views) > 1 { result.Source = "mixed" } switch { case allOK: result.Quality = "ok" result.ErrorCode = "" case anyStale: result.Quality = "stale" case allFailed: result.Quality = "unreachable" } return result } func peerLiveSpeakerIDOrNull(id string) any { if strings.TrimSpace(id) == "" { return nil } return id }