feat(topology): serve bounded route links and identify decoded Public traffic

This commit is contained in:
n30nex committed 2026-10-04 01:03:08 -04:00
1 parent 53f2467864
commit 84ac3c8781
19 files changed
+486

No files matched your search

+1
View File
@@ -20,6 +20,7 @@ import (
func RoutesRouter(reader api.Reader) http.Handler {
r := chi.NewRouter()
r.Get("/", listKnownRoutes(reader))
r.Get("/topology", getTopologyLinks(reader))
r.Get("/cross", searchCrossIATARoutes(reader))
r.Get("/search", searchKnownRoutes(reader))
r.Get("/{iata}/{pathKey}/observations", getRouteEvidence(reader))
@@ -417,3 +417,7 @@ func (s stubReader) GetRouteEvidence(ctx context.Context, iata, key string, q ap
}
return nil, nil
}
func (s stubReader) GetTopologyLinks(_ context.Context, _ []string, _ time.Time, _ time.Time) (*api.TopologyLinks, error) {
return &api.TopologyLinks{}, nil
}
+44
View File
@@ -0,0 +1,44 @@
// Copyright 2026 Beacon Contributors
// SPDX-License-Identifier: AGPL-3.0-or-later
package handlers
import (
"context"
"github.com/MeshCore-Beacon/beacon-server/internal/api"
"net/http"
"time"
)
// getTopologyLinks godoc
// @Summary Unique adjacent route links for a topology snapshot
// @Description Returns up to 100000 undirected node-ID pairs, deduplicated across known routes. Minute-aligned window includes the current minute. Existing rate limits apply; no per-node lookup or route pagination is needed. Capped is true when more links exist.
// @Tags Routes
// @Produce json
// @Param window query string false "History window: 15m (default), 1h, or 24h"
// @Param iatas query string false "Comma-separated reception IATA codes"
// @Success 200 {object} api.TopologyLinks
// @Failure 400 {object} handlers.APIError
// @Failure 503 {object} handlers.APIError
// @Router /routes/topology [get]
func getTopologyLinks(reader api.Reader) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
window := r.URL.Query().Get("window")
if window == "" {
window = "15m"
}
duration, ok := map[string]time.Duration{"15m": 15 * time.Minute, "1h": time.Hour, "24h": 24 * time.Hour}[window]
if !ok {
respondError(w, http.StatusBadRequest, "window must be 15m, 1h or 24h")
return
}
until := time.Now().UTC().Truncate(time.Minute).Add(time.Minute)
ctx, cancel := context.WithTimeout(r.Context(), 15*time.Second)
defer cancel()
links, err := reader.GetTopologyLinks(ctx, parseIATAs(r), until.Add(-duration), until)
if err != nil {
respondError(w, http.StatusServiceUnavailable, "route snapshot unavailable; try again shortly")
return
}
respond(w, http.StatusOK, links)
}
}
+22
View File
@@ -0,0 +1,22 @@
// Copyright 2026 Beacon Contributors
// SPDX-License-Identifier: AGPL-3.0-or-later
package handlers
import (
"net/http/httptest"
"testing"
)
func TestTopologyWindowValidation(t *testing.T) {
for _, window := range []string{"", "15m", "1h", "24h", "72h", "0", "garbage"} {
w := httptest.NewRecorder()
RoutesRouter(stubReader{}).ServeHTTP(w, httptest.NewRequest("GET", "/topology?window="+window, nil))
want := 200
if window == "72h" || window == "0" || window == "garbage" {
want = 400
}
if w.Code != want {
t.Fatalf("%s: %d", window, w.Code)
}
}
}
+2
View File
@@ -224,6 +224,8 @@ type Reader interface {
// GetTraceByTag returns all packets for a given trace tag with resolved routes.
GetTraceByTag(ctx context.Context, tag string) (*TraceDetail, error)
GetTopologyLinks(ctx context.Context, iatas []string, since, until time.Time) (*TopologyLinks, error)
// ListKnownRoutes returns known routes filtered by IATA and optional hop count.
// A nonzero cursorID breaks ties within the cursor's millisecond.
ListKnownRoutes(ctx context.Context, iata string, hopCount int32, cursor time.Time, cursorID int64, limit int32) ([]KnownRoute, error)
+14
View File
@@ -0,0 +1,14 @@
// Copyright 2026 Beacon Contributors
// SPDX-License-Identifier: AGPL-3.0-or-later
package api
import "github.com/google/uuid"
// TopologyLinks is a bounded set of undirected, adjacent known-route segments.
// It does not imply radio reachability or join across missing path entries.
type TopologyLinks struct {
Links [][2]uuid.UUID `json:"links"`
Capped bool `json:"capped"`
Since int64 `json:"since"`
Until int64 `json:"until"`
}