BGP history candidates

GET /bgp/history/candidates

Per-peer candidate RIB intervals for one (prefix, device) over a window — the "why did the best path change" lanes, served from the full-mode per-peer RIB changelog (bgp_rib_entry_history). Each interval carries the candidate's attributes and the recorded is_best, so a client reconstructs the live candidate set (and which one is best) at any instant by interval overlap.

Auth: all authenticated roles

Query params: as_id, prefix, and device_id (all required); from/to (required, RFC 3339); optional routing_domain_id. match is always exact (lanes never follow a covering aggregate).

Response: 200 OK

{
  "full_available": true,
  "candidates": [
    {
      "peer_ip": "198.51.100.1", "peer_as": 65001, "peer_type": "ebgp", "path_id": 0,
      "next_hop": "198.51.100.1", "as_path": "65001 65100", "local_pref": 200, "med": 0,
      "origin": "igp", "is_best": true,
      "valid_from": "2026-07-01T10:00:00Z", "valid_to": "2026-07-01T10:30:00Z"
    }
  ]
}

valid_to is omitted for an open (still-live) interval; local_pref (peer sent none) and communities (empty) are likewise omitted. full_available is false when no full RIB history exists for the window — candidates is then empty (guaranteed: if any candidate exists in the window, full_available reports true even when recording stopped mid-window) and the UI shows a "requires full mode" hint rather than a misleading "no changes" state. Bounded by design: one prefix on one device → at most the peer count × churn.

Errors: 400 if as_id, prefix, device_id, or the window is missing/invalid.