diff --git a/cmd/server/chunked_load.go b/cmd/server/chunked_load.go
index dd626716a..92449dff2 100644
--- a/cmd/server/chunked_load.go
+++ b/cmd/server/chunked_load.go
@@ -663,8 +663,8 @@ func (s *PacketStore) scanAndMergeChunk(rows *sql.Rows, relayPM *prefixMap, cold
// scanned to keep scanArgs aligned with the o.raw_hex column,
// and its header route feeds wireRoute (#76).
Timestamp: normalizeTimestamp(nullStrVal(obsTimestamp)),
- wireRoute: wireRouteFromRawHex(obsRawHex.String),
}
+ cacheObservationWireEvidence(obs, obsRawHex.String)
// Same relay-hop indexing as Load and live ingest (see
// indexObservationRelayHops). relayPM is passed in from
diff --git a/cmd/server/openapi.go b/cmd/server/openapi.go
index af1f51f1e..6ce55eee3 100644
--- a/cmd/server/openapi.go
+++ b/cmd/server/openapi.go
@@ -55,6 +55,7 @@ func routeDescriptions() map[string]routeMeta {
Errors: []routeError{{Status: "400", Description: "Invalid window."}, {Status: "500", Description: "Current identity or node metadata could not be read."}, {Status: "503", Description: "Relay snapshot is unavailable or warming up."}}},
"GET /api/regions/history": {Summary: "Hourly scoped relay history", Description: "One bounded bulk snapshot for Regions timeline and equal-window comparison. Includes 504 UTC clock-hour buckets over 21 days, excluding the unfinished current hour. Counts distinct transmissions per confirmed node and scope, assigned by transmission FirstSeen; summed node counts are relay appearances. Metadata is current, or archived only when no current row exists. Current catalog identity resolution and live visibility policies apply. Coverage and truncation must be checked before interpreting changes.", Tag: "analytics", Response: schemaRef("RegionsHistoryResponse"),
Errors: []routeError{{Status: "500", Description: "Identity or node metadata could not be read."}, {Status: "503", Description: "Relay history snapshot is unavailable or warming up."}}},
+ "GET /api/regions/routes": {Summary: "Observed directed scoped relay steps", Description: "Paired Regions history plus bounded directed adjacent relay steps from each explicitly observed scoped flood path. The observation transport codes must match the canonical transmission frame underlying its existing scope name. Counts deduplicate transmission IDs per directed step and scope and use FirstSeen hourly buckets. Scope handoffs, RF links, complete end-to-end paths and observer/origin edges are not inferred. Independent stepNodes carry current or archived endpoint metadata. Snapshot-wide diagnostics expose skipped or incomplete evidence.", Tag: "analytics", Response: schemaRef("RegionsRoutesResponse"), Errors: []routeError{{Status: "500", Description: "Identity or endpoint metadata could not be read."}, {Status: "503", Description: "Paired relay routes snapshot is unavailable or warming up."}}},
"GET /api/config/theme": {Summary: "Get theme configuration", Description: "Returns color maps, CSS variables, and theme defaults.", Tag: "config"},
"GET /api/config/map": {Summary: "Get map configuration", Tag: "config"},
"GET /api/config/geo-filter": {Summary: "Get geo-filter configuration", Tag: "config"},
@@ -1018,6 +1019,9 @@ func componentSchemas() map[string]interface{} {
for name, schema := range regionsHistorySchemas() {
schemas[name] = schema
}
+ for name, schema := range regionsRoutesSchemas() {
+ schemas[name] = schema
+ }
for name, schema := range heardBySchemas() {
schemas[name] = schema
}
diff --git a/cmd/server/regions_history.go b/cmd/server/regions_history.go
index 625466b9b..c9b120f92 100644
--- a/cmd/server/regions_history.go
+++ b/cmd/server/regions_history.go
@@ -1,6 +1,7 @@
package main
import (
+ "context"
"log"
"net/http"
"sort"
@@ -215,6 +216,22 @@ func (s *Server) handleRegionsHistory(w http.ResponseWriter, r *http.Request) {
writeError(w, http.StatusServiceUnavailable, "observed relay history is warming up")
return
}
+ response, err := s.regionsHistoryResponse(r.Context(), snapshot)
+ if err != nil {
+ log.Printf("[regions-history] node metadata: %v", err)
+ writeError(w, http.StatusInternalServerError, "could not read region node metadata")
+ return
+ }
+ w.Header().Set("Cache-Control", "no-store")
+ writeJSON(w, response)
+}
+
+func (s *Server) regionsHistoryResponse(ctx context.Context, snapshot *repeaterRelaySnapshot) (RegionsHistoryResponse, error) {
+ response, _, err := s.regionsHistoryAndStepNodes(ctx, snapshot, nil)
+ return response, err
+}
+
+func (s *Server) regionsHistoryAndStepNodes(ctx context.Context, snapshot *repeaterRelaySnapshot, routeKeys map[string]bool) (RegionsHistoryResponse, []RegionsRouteNode, error) {
h := snapshot.history
response := RegionsHistoryResponse{GeneratedAt: snapshot.generatedAt.Format(time.RFC3339Nano),
HistoryStart: h.start.Format(time.RFC3339Nano), HistoryEnd: h.end.Format(time.RFC3339Nano),
@@ -225,18 +242,27 @@ func (s *Server) handleRegionsHistory(w http.ResponseWriter, r *http.Request) {
start := snapshot.coverageStart.Format(time.RFC3339Nano)
response.Coverage.Start = &start
}
- keys := make([]string, 0, len(h.nodes))
+ keys := make([]string, 0, len(h.nodes)+len(routeKeys))
for key := range h.nodes {
keys = append(keys, key)
}
- err := s.forEachVisibleRegionNode(r.Context(), keys, true, func(n observedRegionNodeMetadata) bool {
+ for key := range routeKeys {
+ if _, exists := h.nodes[key]; !exists {
+ keys = append(keys, key)
+ }
+ }
+ stepNodes := []RegionsRouteNode{}
+ err := s.forEachVisibleRegionNode(ctx, keys, true, func(n observedRegionNodeMetadata) bool {
+ if routeKeys[n.publicKey] {
+ stepNodes = append(stepNodes, regionRouteNodeMetadata(n))
+ }
scopes := h.nodes[n.publicKey]
if len(scopes) == 0 {
return true
}
if len(response.Nodes) >= maxObservedRegionNodes {
response.Truncated = true
- return false
+ return len(routeKeys) > 0
}
node := RegionsHistoryNode{PublicKey: n.publicKey, Name: n.name, Role: n.role,
PositionSource: "none", MetadataSource: n.source, Scopes: scopes}
@@ -250,13 +276,7 @@ func (s *Server) handleRegionsHistory(w http.ResponseWriter, r *http.Request) {
response.Nodes = append(response.Nodes, node)
return true
})
- if err != nil {
- log.Printf("[regions-history] node metadata: %v", err)
- writeError(w, http.StatusInternalServerError, "could not read region node metadata")
- return
- }
- w.Header().Set("Cache-Control", "no-store")
- writeJSON(w, response)
+ return response, stepNodes, err
}
func regionsHistorySchemas() map[string]*openAPISchema {
diff --git a/cmd/server/regions_observed.go b/cmd/server/regions_observed.go
index 38a4c397a..f5657b24a 100644
--- a/cmd/server/regions_observed.go
+++ b/cmd/server/regions_observed.go
@@ -22,6 +22,7 @@ type regionScopeRelay struct {
type repeaterRelaySnapshot struct {
info map[string]RepeaterRelayInfo
history *regionsRelayHistory
+ routes *regionsRouteSnapshot
generatedAt time.Time
coverageStart time.Time
loading bool
diff --git a/cmd/server/regions_routes.go b/cmd/server/regions_routes.go
new file mode 100644
index 000000000..325271034
--- /dev/null
+++ b/cmd/server/regions_routes.go
@@ -0,0 +1,334 @@
+package main
+
+import (
+ "encoding/json"
+ "log"
+ "net/http"
+ "sort"
+ "strconv"
+ "time"
+
+ "github.com/meshcore-analyzer/regions"
+)
+
+const (
+ maxRegionsRouteSteps = 5000
+ maxRegionsRouteCells = 100000
+ maxRegionsRoutePaths = 250000
+ maxRegionsRoutePathsPerTx = 128
+ maxRegionsRoutePairsPerTx = 8192
+ maxRegionsRoutePathBytes = 8192
+)
+
+// Cache only the observation's own header/transport evidence. Full raw frames
+// remain discarded. The two added fields fit StoreObs's existing padding.
+func cacheObservationWireEvidence(o *StoreObs, raw string) {
+ o.wireRoute = wireRouteFromRawHex(raw)
+ o.wireTransport, o.transportKnown = wireTransportCodes(raw)
+}
+
+func wireTransportCodes(raw string) (uint32, bool) {
+ route := wireRouteFromRawHex(raw)
+ if (route != 1 && route != 4) || len(raw) < 10 {
+ return 0, false
+ }
+ value, err := strconv.ParseUint(raw[2:10], 16, 32)
+ for _, c := range raw[2:10] {
+ if !((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')) {
+ return 0, false
+ }
+ }
+ return uint32(value), err == nil
+}
+
+type RegionsRouteStep struct {
+ From string `json:"from"`
+ To string `json:"to"`
+ Scope string `json:"scope"`
+ Buckets []RegionsHistoryBucket `json:"buckets"`
+}
+
+type RegionsStepEvidence struct {
+ SkippedObservations int `json:"skippedObservations"`
+ UnresolvedHops int `json:"unresolvedHops"`
+ SourceTruncated bool `json:"sourceTruncated"`
+ Incomplete bool `json:"incomplete"`
+ MissingEndpointSteps int `json:"missingEndpointSteps"`
+}
+
+type RegionsRouteNode struct {
+ PublicKey string `json:"publicKey"`
+ Name string `json:"name"`
+ Role string `json:"role"`
+ Lat *float64 `json:"lat"`
+ Lon *float64 `json:"lon"`
+ PositionSource string `json:"positionSource"`
+ MetadataSource string `json:"metadataSource"`
+}
+
+func regionRouteNodeMetadata(n observedRegionNodeMetadata) RegionsRouteNode {
+ node := RegionsRouteNode{PublicKey: n.publicKey, Name: n.name, Role: n.role, PositionSource: "none", MetadataSource: n.source}
+ if node.Name == "" {
+ node.Name = n.publicKey
+ }
+ if reportedRegionPosition(n.lat, n.lon) {
+ lat, lon := n.lat.Float64, n.lon.Float64
+ node.Lat, node.Lon, node.PositionSource = &lat, &lon, "reported"
+ }
+ return node
+}
+
+type RegionsRoutesResponse struct {
+ RegionsHistoryResponse
+ Steps []RegionsRouteStep `json:"steps"`
+ StepNodes []RegionsRouteNode `json:"stepNodes"`
+ StepsTruncated bool `json:"stepsTruncated"`
+ StepBucketsTruncated bool `json:"stepBucketsTruncated"`
+ StepEvidence RegionsStepEvidence `json:"stepEvidence"`
+}
+
+type regionsRouteSnapshot struct {
+ steps []RegionsRouteStep
+ stepsTruncated, bucketsTruncated bool
+ evidence RegionsStepEvidence
+}
+
+type regionsRoutePath struct {
+ path string
+ observations int
+}
+type regionsRouteTx struct {
+ id int
+ when time.Time
+ scope string
+ paths []regionsRoutePath
+}
+type regionsRoutesCollector struct {
+ start, end time.Time
+ txs []regionsRouteTx
+ pathCount int
+ evidence RegionsStepEvidence
+}
+
+func newRegionsRoutesCollector(now time.Time) *regionsRoutesCollector {
+ end := now.UTC().Truncate(time.Hour)
+ return ®ionsRoutesCollector{start: end.Add(-regionsHistoryBucketCount * time.Hour), end: end}
+}
+
+// capture copies immutable strings and scalar wire evidence while the packet
+// lock is held. Path parsing, identity resolution and aggregation run later.
+// It is called once per distinct transmission by the shared relay collector.
+func (c *regionsRoutesCollector) capture(tx *StoreTx) {
+ when, valid := parseRelayTS(tx.FirstSeen)
+ c.captureAt(tx, when, valid)
+}
+
+func (c *regionsRoutesCollector) captureAt(tx *StoreTx, when time.Time, valid bool) {
+ if tx.PayloadType == nil || *tx.PayloadType == payloadTypeAdvert || *tx.PayloadType == 9 || regions.ValidateCandidate(tx.ScopeName) != nil {
+ return
+ }
+ if !valid || when.Before(c.start) || !when.Before(c.end) {
+ return
+ }
+ canonical, known := wireTransportCodes(tx.RawHex)
+ known = known && canonical>>16 != 0
+ item := regionsRouteTx{id: tx.ID, when: when, scope: tx.ScopeName}
+ seen := make(map[string]int)
+ for _, obs := range tx.Observations {
+ if obs == nil {
+ continue
+ }
+ if obs.wireRoute == 0 {
+ c.evidence.SkippedObservations++
+ continue
+ }
+ if obs.wireRoute != 1 {
+ continue
+ } // Only explicit TRANSPORT_FLOOD observation headers.
+ if !known || !obs.transportKnown || obs.wireTransport != canonical || len(obs.PathJSON) > maxRegionsRoutePathBytes {
+ c.evidence.SkippedObservations++
+ continue
+ }
+ if index, exists := seen[obs.PathJSON]; exists {
+ item.paths[index].observations++
+ continue
+ }
+ if len(item.paths) >= maxRegionsRoutePathsPerTx || c.pathCount >= maxRegionsRoutePaths {
+ c.evidence.SourceTruncated = true
+ c.evidence.SkippedObservations++
+ continue
+ }
+ seen[obs.PathJSON] = len(item.paths)
+ item.paths = append(item.paths, regionsRoutePath{path: obs.PathJSON, observations: 1})
+ c.pathCount++
+ }
+ if len(tx.Observations) == 0 && tx.PathJSON != "" {
+ c.evidence.SkippedObservations++
+ }
+ if len(item.paths) > 0 {
+ c.txs = append(c.txs, item)
+ }
+}
+
+type regionsRouteKey struct{ from, to, scope string }
+type regionsRouteCell struct {
+ count int
+ last time.Time
+}
+
+func (c *regionsRoutesCollector) build(pm *prefixMap) *regionsRouteSnapshot {
+ out := ®ionsRouteSnapshot{steps: []RegionsRouteStep{}, evidence: c.evidence}
+ sort.Slice(c.txs, func(i, j int) bool { return c.txs[i].id < c.txs[j].id })
+ resolve := newRelayTokenResolver(pm)
+ rows := make(map[regionsRouteKey]map[int]regionsRouteCell)
+ seen := make(map[regionsRouteKey]bool)
+ cellCount := 0
+ for _, tx := range c.txs {
+ clear(seen)
+ index := int(tx.when.Sub(c.start) / time.Hour)
+ for _, path := range tx.paths {
+ var tokens []string
+ if err := json.Unmarshal([]byte(path.path), &tokens); err != nil || len(tokens) > 64 {
+ out.evidence.SkippedObservations += path.observations
+ continue
+ }
+ previous := ""
+ for _, token := range tokens {
+ if len(resolve.cache) >= 4096 {
+ clear(resolve.cache)
+ }
+ key := resolve.key(token)
+ if key == "" {
+ previous = ""
+ out.evidence.UnresolvedHops++
+ continue
+ }
+ if previous != "" && previous != key {
+ step := regionsRouteKey{previous, key, tx.scope}
+ if !seen[step] {
+ if len(seen) >= maxRegionsRoutePairsPerTx {
+ out.stepsTruncated = true
+ previous = key
+ continue
+ }
+ seen[step] = true
+ byIndex := rows[step]
+ if byIndex == nil {
+ if len(rows) >= maxRegionsRouteSteps {
+ out.stepsTruncated = true
+ previous = key
+ continue
+ }
+ byIndex = make(map[int]regionsRouteCell)
+ }
+ cell, exists := byIndex[index]
+ if !exists {
+ if cellCount >= maxRegionsRouteCells {
+ out.bucketsTruncated = true
+ previous = key
+ continue
+ }
+ cellCount++
+ }
+ cell.count++
+ if tx.when.After(cell.last) {
+ cell.last = tx.when
+ }
+ byIndex[index] = cell
+ rows[step] = byIndex
+ }
+ }
+ previous = key
+ }
+ }
+ }
+ keys := make([]regionsRouteKey, 0, len(rows))
+ for key := range rows {
+ keys = append(keys, key)
+ }
+ sort.Slice(keys, func(i, j int) bool {
+ a, b := keys[i], keys[j]
+ if a.from != b.from {
+ return a.from < b.from
+ }
+ if a.to != b.to {
+ return a.to < b.to
+ }
+ return a.scope < b.scope
+ })
+ for _, key := range keys {
+ byIndex := rows[key]
+ indexes := make([]int, 0, len(byIndex))
+ for index := range byIndex {
+ indexes = append(indexes, index)
+ }
+ sort.Ints(indexes)
+ step := RegionsRouteStep{From: key.from, To: key.to, Scope: key.scope, Buckets: make([]RegionsHistoryBucket, 0, len(indexes))}
+ for _, index := range indexes {
+ cell := byIndex[index]
+ step.Buckets = append(step.Buckets, RegionsHistoryBucket{Index: index, Count: cell.count, LastRelayed: cell.last.UTC().Format(time.RFC3339Nano)})
+ }
+ out.steps = append(out.steps, step)
+ }
+ out.evidence.Incomplete = out.evidence.SkippedObservations > 0 || out.evidence.UnresolvedHops > 0 || out.evidence.SourceTruncated || out.stepsTruncated || out.bucketsTruncated
+ return out
+}
+
+func (s *Server) handleRegionsRoutes(w http.ResponseWriter, r *http.Request) {
+ if s.store == nil || s.db == nil || s.db.conn == nil {
+ writeError(w, http.StatusServiceUnavailable, "observed relay data unavailable")
+ return
+ }
+ s.store.repeaterEnrichMu.Lock()
+ snapshot := s.store.repeaterRelaySnapshot
+ s.store.repeaterEnrichMu.Unlock()
+ if snapshot == nil || snapshot.history == nil || snapshot.routes == nil {
+ writeError(w, http.StatusServiceUnavailable, "observed relay routes are warming up")
+ return
+ }
+ keys := make(map[string]bool, len(snapshot.routes.steps)*2)
+ for _, step := range snapshot.routes.steps {
+ keys[step.From] = true
+ keys[step.To] = true
+ }
+ history, stepNodes, err := s.regionsHistoryAndStepNodes(r.Context(), snapshot, keys)
+ if err != nil {
+ log.Printf("[regions-routes] metadata: %v", err)
+ writeError(w, http.StatusInternalServerError, "could not read region node metadata")
+ return
+ }
+ visible := make(map[string]bool, len(stepNodes))
+ for _, node := range stepNodes {
+ visible[node.PublicKey] = true
+ }
+ response := RegionsRoutesResponse{RegionsHistoryResponse: history, Steps: []RegionsRouteStep{}, StepNodes: stepNodes, StepsTruncated: snapshot.routes.stepsTruncated, StepBucketsTruncated: snapshot.routes.bucketsTruncated, StepEvidence: snapshot.routes.evidence}
+ for _, step := range snapshot.routes.steps {
+ if visible[step.From] && visible[step.To] {
+ response.Steps = append(response.Steps, step)
+ } else {
+ response.StepEvidence.MissingEndpointSteps++
+ response.StepEvidence.Incomplete = true
+ }
+ }
+ w.Header().Set("Cache-Control", "no-store")
+ writeJSON(w, response)
+}
+
+func regionsRoutesSchemas() map[string]*openAPISchema {
+ properties := make(map[string]*openAPISchema)
+ for key, value := range regionsHistorySchemas()["RegionsHistoryResponse"].Properties {
+ properties[key] = value
+ }
+ properties["steps"] = &openAPISchema{Type: "array", Items: openAPIRef("RegionsRouteStep")}
+ properties["stepNodes"] = &openAPISchema{Type: "array", Items: openAPIRef("RegionsRouteNode"), Description: "At most 10000 eligible visible endpoints, independent of legacy relay history scope attribution."}
+ properties["stepsTruncated"] = &openAPISchema{Type: "boolean"}
+ properties["stepBucketsTruncated"] = &openAPISchema{Type: "boolean"}
+ properties["stepEvidence"] = openAPIRef("RegionsStepEvidence")
+ return map[string]*openAPISchema{
+ "RegionsRoutesResponse": {Type: "object", Properties: properties},
+ "RegionsRouteStep": {Type: "object", Properties: map[string]*openAPISchema{"from": {Type: "string"}, "to": {Type: "string"}, "scope": {Type: "string"}, "buckets": {Type: "array", Items: openAPIRef("RegionsRouteBucket"), Description: "Counts are not additive unique packet totals."}}},
+ "RegionsRouteBucket": {Type: "object", Properties: map[string]*openAPISchema{"index": {Type: "integer", Description: "Zero-based completed UTC hour, 0 through 503."}, "count": {Type: "integer", Description: "Distinct transmission IDs for this directed adjacent relay step and scope."}, "lastRelayed": {Type: "string", Description: "Latest transmission FirstSeen in this bucket, RFC3339."}}},
+ "RegionsRouteNode": {Type: "object", Properties: map[string]*openAPISchema{"publicKey": {Type: "string"}, "name": {Type: "string"}, "role": {Type: "string"}, "lat": {Type: "number", Nullable: true}, "lon": {Type: "number", Nullable: true}, "positionSource": {Type: "string", Enum: []string{"reported", "none"}}, "metadataSource": {Type: "string", Enum: []string{"current", "archived"}}}},
+ "RegionsStepEvidence": {Type: "object", Description: "Snapshot-wide diagnostics, not selected-window totals or scope-transition evidence.", Properties: map[string]*openAPISchema{"skippedObservations": {Type: "integer"}, "unresolvedHops": {Type: "integer"}, "sourceTruncated": {Type: "boolean"}, "incomplete": {Type: "boolean"}, "missingEndpointSteps": {Type: "integer"}}},
+ }
+}
diff --git a/cmd/server/regions_routes_test.go b/cmd/server/regions_routes_test.go
new file mode 100644
index 000000000..608947c84
--- /dev/null
+++ b/cmd/server/regions_routes_test.go
@@ -0,0 +1,499 @@
+package main
+
+import (
+ "encoding/json"
+ "fmt"
+ "net/http/httptest"
+ "reflect"
+ "strings"
+ "testing"
+ "time"
+ "unsafe"
+)
+
+func routeObservation(path, raw string) *StoreObs {
+ o := &StoreObs{PathJSON: path}
+ cacheObservationWireEvidence(o, raw)
+ return o
+}
+
+func routeTransmission(id int, when time.Time, paths ...string) *StoreTx {
+ pt, rt := 5, 0
+ tx := &StoreTx{ID: id, FirstSeen: when.Format(time.RFC3339Nano), PayloadType: &pt, RouteType: &rt, ScopeName: "#sample", RawHex: "14aabbccdd00"}
+ for _, path := range paths {
+ tx.Observations = append(tx.Observations, routeObservation(path, "14aabbccdd00"))
+ }
+ return tx
+}
+
+func TestRegionRoutesWireEvidenceAndLayout(t *testing.T) {
+ if unsafe.Sizeof(uintptr(0)) == 8 && unsafe.Sizeof(StoreObs{}) != 200 {
+ t.Fatalf("observation grew: %d", unsafe.Sizeof(StoreObs{}))
+ }
+ var layout StoreObs
+ if unsafe.Offsetof(layout.transportKnown) != unsafe.Offsetof(layout.wireRoute)+1 || unsafe.Offsetof(layout.wireTransport)+unsafe.Sizeof(layout.wireTransport) > unsafe.Sizeof(layout) {
+ t.Fatal("transport evidence no longer fits trailing padding")
+ }
+ for _, tc := range []struct {
+ raw string
+ known bool
+ route uint8
+ }{
+ {"14AABBCCDD00", true, 1}, {"17aabbccdd00", true, 4}, {"1500", false, 2}, {"", false, 0},
+ {"14aa", false, 1}, {"14aabbzzdd00", false, 1}, {"xxaabbccdd00", false, 0},
+ } {
+ o := routeObservation("[]", tc.raw)
+ if o.wireRoute != tc.route || o.transportKnown != tc.known {
+ t.Fatalf("%q: route=%d known=%v", tc.raw, o.wireRoute, o.transportKnown)
+ }
+ if tc.known && o.wireTransport != 0xaabbccdd {
+ t.Fatal("both transport codes were not retained")
+ }
+ }
+}
+
+func TestRegionRoutesDirectionDedupAndNoStitching(t *testing.T) {
+ now := time.Date(2026, 10, 16, 12, 35, 0, 0, time.UTC)
+ a, b, c := strings.Repeat("ab", 32), strings.Repeat("cd", 32), strings.Repeat("ef", 32)
+ ab, ba := `["`+a+`","`+b+`"]`, `["`+b+`","`+a+`"]`
+ tx := routeTransmission(1, now.Add(-2*time.Hour), ab, ab, ba, `["`+a+`","`+b+`","`+a+`","`+b+`"]`)
+ collector := newRegionsRoutesCollector(now)
+ collector.capture(tx)
+ collector.capture(routeTransmission(2, now.Add(-2*time.Hour), `["`+b+`"]`, `["`+c+`"]`))
+ got := collector.build(nil)
+ if len(got.steps) != 2 || got.steps[0].From != a || got.steps[0].To != b || got.steps[1].From != b || got.steps[1].To != a {
+ t.Fatalf("direction/stitching: %+v", got.steps)
+ }
+ for _, step := range got.steps {
+ if len(step.Buckets) != 1 || step.Buckets[0].Count != 1 {
+ t.Fatalf("duplicate observation/loop inflated count: %+v", step)
+ }
+ }
+}
+
+func TestRegionRoutesStrictObservationScopeAndBounds(t *testing.T) {
+ now := time.Date(2026, 10, 16, 12, 35, 0, 0, time.UTC)
+ a, b := strings.Repeat("ab", 32), strings.Repeat("cd", 32)
+ path := `["` + a + `","` + b + `"]`
+ collector := newRegionsRoutesCollector(now)
+ end := now.Truncate(time.Hour)
+ valid := routeTransmission(1, end.Add(-time.Nanosecond), path)
+ direct := 3
+ valid.RouteType = &direct
+ valid.RawHex = "17aabbccdd00" // canonical direct does not invalidate an explicitly observed flood.
+ collector.capture(valid)
+ for i, raw := range []string{"", "14aabbccde00", "14aabb", "1500", "17aabbccdd00"} {
+ tx := routeTransmission(i+2, end.Add(-time.Minute))
+ tx.Observations = []*StoreObs{routeObservation(path, raw)}
+ collector.capture(tx)
+ }
+ for i, when := range []time.Time{end, now.Add(time.Hour), end.Add(-21*24*time.Hour - time.Nanosecond)} {
+ collector.capture(routeTransmission(i+10, when, path))
+ }
+ for i, pt := range []int{payloadTypeAdvert, 9} {
+ tx := routeTransmission(i+20, end.Add(-time.Minute), path)
+ tx.PayloadType = &pt
+ collector.capture(tx)
+ }
+ got := collector.build(nil)
+ if len(got.steps) != 1 || got.steps[0].Buckets[0].Count != 1 || got.steps[0].Buckets[0].Index != 503 {
+ t.Fatalf("unsafe observation or time attribution: %+v", got.steps)
+ }
+ if !got.evidence.Incomplete || got.evidence.SkippedObservations != 3 {
+ t.Fatalf("missing evidence diagnostic: %+v", got.evidence)
+ }
+}
+
+func TestRegionRoutesUnknownHopsBreakAdjacency(t *testing.T) {
+ now := time.Date(2026, 10, 16, 12, 35, 0, 0, time.UTC)
+ a, b := strings.Repeat("ab", 32), strings.Repeat("cd", 32)
+ collector := newRegionsRoutesCollector(now)
+ collector.capture(routeTransmission(1, now.Add(-time.Hour), `["`+a+`","unknown","`+b+`"]`))
+ got := collector.build(nil)
+ if len(got.steps) != 0 || got.evidence.UnresolvedHops != 1 || !got.evidence.Incomplete {
+ t.Fatalf("bridged unknown hop: %+v", got)
+ }
+}
+
+func TestRegionRoutesZeroScopeMalformedAndDifferentTransportCodes(t *testing.T) {
+ now := time.Date(2026, 10, 16, 12, 35, 0, 0, time.UTC)
+ a, b := strings.Repeat("ab", 32), strings.Repeat("cd", 32)
+ path := `["` + a + `","` + b + `"]`
+ collector := newRegionsRoutesCollector(now)
+ zero := routeTransmission(1, now.Add(-time.Hour))
+ zero.RawHex = "140000000000"
+ zero.Observations = []*StoreObs{routeObservation(path, zero.RawHex)}
+ collector.capture(zero)
+ for i, raw := range []string{"14aabcccdd00", "14aabbccde00"} {
+ tx := routeTransmission(i+2, now.Add(-time.Hour))
+ tx.Observations = []*StoreObs{routeObservation(path, raw)}
+ collector.capture(tx)
+ }
+ collector.capture(routeTransmission(4, now.Add(-time.Hour), `["`+a+`",null,"`+b+`"]`, `["`+a+`","`+b+`" BROKEN]`))
+ got := collector.build(nil)
+ if len(got.steps) != 0 || !got.evidence.Incomplete || got.evidence.SkippedObservations != 4 || got.evidence.UnresolvedHops != 1 {
+ t.Fatalf("invalid scope/path accepted: %+v", got)
+ }
+ // A collided prefix remains unknown, including when one candidate is hidden
+ // or a heuristic resolver would prefer a geographically close candidate.
+ pm := buildPrefixMap([]nodeInfo{{PublicKey: a, Role: "repeater"}, {PublicKey: "ab" + strings.Repeat("34", 31), Role: "repeater"}, {PublicKey: b, Role: "repeater"}})
+ collector = newRegionsRoutesCollector(now)
+ collector.capture(routeTransmission(5, now.Add(-time.Hour), `["`+b+`","ab","`+a+`"]`))
+ if got := collector.build(pm); len(got.steps) != 0 {
+ t.Fatal("collision bridged")
+ }
+ pm.nonRelay = map[string]struct{}{a: {}}
+ collector = newRegionsRoutesCollector(now)
+ collector.capture(routeTransmission(6, now.Add(-time.Hour), path))
+ if got := collector.build(pm); len(got.steps) != 0 {
+ t.Fatal("listener-only identity accepted as relay")
+ }
+}
+
+func TestRegionRoutesTransportCachedOnEveryObservationPath(t *testing.T) {
+ db := routeMaskServerDB(t)
+ defer db.Close()
+ rmSeed(t, db)
+ for id, raw := range map[int]string{1: "14aabbccdd00", 2: "17aabbccde00", 3: "14aabbccdf00", 4: "1600"} {
+ if _, err := db.conn.Exec("UPDATE observations SET raw_hex=? WHERE id=?", raw, id); err != nil {
+ t.Fatal(err)
+ }
+ }
+ type evidence struct {
+ Route uint8
+ Transport uint32
+ Known bool
+ }
+ snapshot := func(s *PacketStore) map[int]evidence {
+ s.mu.RLock()
+ defer s.mu.RUnlock()
+ out := map[int]evidence{}
+ for _, tx := range s.packets {
+ for _, o := range tx.Observations {
+ out[o.ID] = evidence{o.wireRoute, o.wireTransport, o.transportKnown}
+ if o.RawHex != "" {
+ t.Fatal("duplicate raw frame retained")
+ }
+ }
+ }
+ return out
+ }
+ want := map[int]evidence{1: {1, 0xaabbccdd, true}, 2: {4, 0xaabbccde, true}, 3: {1, 0xaabbccdf, true}, 4: {3, 0, false}}
+ load := NewPacketStore(db, nil)
+ if err := load.Load(); err != nil {
+ t.Fatal(err)
+ }
+ chunked := NewPacketStore(db, nil)
+ if err := chunked.LoadChunked(1); err != nil {
+ t.Fatal(err)
+ }
+ window := NewPacketStore(db, nil)
+ if err := window.loadChunk(time.Now().Add(-48*time.Hour), time.Now().Add(time.Hour)); err != nil {
+ t.Fatal(err)
+ }
+ for name, s := range map[string]*PacketStore{"Load": load, "LoadChunked": chunked, "loadChunk": window} {
+ if got := snapshot(s); !reflect.DeepEqual(got, want) {
+ t.Fatalf("%s: %+v", name, got)
+ }
+ }
+ stamp := time.Now().UTC().Add(-30 * time.Minute)
+ rmInsertTx(t, db, 4, "route-live", 0, 1, stamp.Format(time.RFC3339))
+ rmInsertObs(t, db, 5, 4, 1, `[]`, "14aabbcce000", stamp.Unix())
+ load.IngestNewFromDB(3, 100)
+ rmInsertObs(t, db, 6, 2, 2, `["ab"]`, "14aabbcce100", stamp.Unix())
+ rmInsertObs(t, db, 7, 2, 1, `["cd"]`, "", stamp.Unix())
+ load.IngestNewObservations(5, 100)
+ want[5], want[6], want[7] = evidence{1, 0xaabbcce0, true}, evidence{1, 0xaabbcce1, true}, evidence{0, 0, false}
+ if got := snapshot(load); !reflect.DeepEqual(got, want) {
+ t.Fatalf("live hydration: %+v", got)
+ }
+}
+
+func regionsRoutesFixture(t testing.TB) (*Server, time.Time, string, string) {
+ srv, now, a := regionsObservedFixture(t)
+ b := strings.Repeat("cd", 32)
+ if _, err := srv.db.conn.Exec("INSERT INTO nodes(public_key,name,role,lat,lon) VALUES(?,'Second synthetic relay','repeater',55.3,10.4)", b); err != nil {
+ t.Fatal(err)
+ }
+ path := `["` + a + `","` + b + `"]`
+ tx := routeTransmission(1, now.Add(-2*time.Hour), path)
+ // Canonical direct with an independently observed flood must work publicly,
+ // even though the unchanged old relay-history path does not include it.
+ direct := 3
+ tx.RouteType = &direct
+ tx.RawHex = "17aabbccdd00"
+ tx.PathJSON = path
+ srv.store.byPathHop[a] = []*StoreTx{tx}
+ srv.store.byPathHop[b] = []*StoreTx{tx}
+ recomputeRepeaterEnrichmentSafe(srv.store, 24)
+ return srv, now, a, b
+}
+
+func getRegionsRoutes(t testing.TB, srv *Server) RegionsRoutesResponse {
+ t.Helper()
+ w := httptest.NewRecorder()
+ srv.handleRegionsRoutes(w, httptest.NewRequest("GET", "/api/regions/routes", nil))
+ var out RegionsRoutesResponse
+ if w.Code != 200 || json.Unmarshal(w.Body.Bytes(), &out) != nil {
+ t.Fatalf("routes response %d: %s", w.Code, w.Body.String())
+ }
+ if w.Header().Get("Cache-Control") != "no-store" {
+ t.Fatal("missing fresh visibility policy")
+ }
+ return out
+}
+
+func TestRegionsRoutesPairedMetadataVisibilityAndWarmLock(t *testing.T) {
+ srv, _, a, b := regionsRoutesFixture(t)
+ got := getRegionsRoutes(t, srv)
+ if len(got.Nodes) != 0 || len(got.StepNodes) != 2 || len(got.Steps) != 1 || got.Steps[0].From != a || got.Steps[0].To != b || got.Steps[0].Buckets[0].Count != 1 {
+ t.Fatalf("canonical direct scoped flood lost: %+v", got)
+ }
+ srv.store.mu.Lock()
+ done := make(chan *httptest.ResponseRecorder, 1)
+ go func() {
+ w := httptest.NewRecorder()
+ srv.handleRegionsRoutes(w, httptest.NewRequest("GET", "/api/regions/routes", nil))
+ done <- w
+ }()
+ select {
+ case w := <-done:
+ if w.Code != 200 {
+ t.Errorf("warm response=%d", w.Code)
+ }
+ case <-time.After(2 * time.Second):
+ srv.store.mu.Unlock()
+ t.Fatal("warm routes acquired packet lock")
+ }
+ srv.store.mu.Unlock()
+ srv.cfg.SetNodeBlacklist([]string{a})
+ if hidden := getRegionsRoutes(t, srv); len(hidden.Steps) != 0 || len(hidden.StepNodes) != 1 {
+ t.Fatal("hidden endpoint or step leaked")
+ }
+ srv.cfg.SetNodeBlacklist(nil)
+ srv.cfg.SetHiddenNamePrefixes([]string{"Hidden"})
+ if _, err := srv.db.conn.Exec("INSERT INTO observers(id,name) VALUES(?,'Hidden alias')", strings.ToUpper(b)); err != nil {
+ t.Fatal(err)
+ }
+ if hidden := getRegionsRoutes(t, srv); len(hidden.Steps) != 0 || len(hidden.StepNodes) != 1 {
+ t.Fatal("observer alias endpoint leaked")
+ }
+ srv.cfg.SetHiddenNamePrefixes(nil)
+ if _, err := srv.db.conn.Exec(`CREATE TABLE inactive_nodes(public_key TEXT PRIMARY KEY,name TEXT,role TEXT,lat REAL,lon REAL,foreign_advert INTEGER);
+ INSERT INTO inactive_nodes SELECT public_key,name,role,lat,lon,foreign_advert FROM nodes; DELETE FROM nodes`); err != nil {
+ t.Fatal(err)
+ }
+ archived := getRegionsRoutes(t, srv)
+ if len(archived.Steps) != 1 || len(archived.StepNodes) != 2 || archived.StepNodes[0].MetadataSource != "archived" {
+ t.Fatal("archived endpoint metadata lost")
+ }
+ if _, err := srv.db.conn.Exec("INSERT INTO nodes(public_key,name,role) VALUES(?,'Current companion','companion')", a); err != nil {
+ t.Fatal(err)
+ }
+ if current := getRegionsRoutes(t, srv); len(current.Steps) != 0 || len(current.StepNodes) != 1 || current.StepEvidence.MissingEndpointSteps != 1 {
+ t.Fatal("archive restored an endpoint whose current role is companion")
+ }
+}
+
+func TestRegionRoutesSourceAndRowBounds(t *testing.T) {
+ now := time.Date(2026, 10, 16, 12, 35, 0, 0, time.UTC)
+ a, b := strings.Repeat("ab", 32), strings.Repeat("cd", 32)
+ path := `["` + a + `","` + b + `"]`
+ tx := routeTransmission(1, now.Add(-time.Hour))
+ for i := 0; i < maxRegionsRoutePathsPerTx+1; i++ {
+ tx.Observations = append(tx.Observations, routeObservation(strings.Repeat(" ", i)+path, "14aabbccdd00"))
+ }
+ collector := newRegionsRoutesCollector(now)
+ collector.capture(tx)
+ if collector.pathCount != maxRegionsRoutePathsPerTx || !collector.evidence.SourceTruncated {
+ t.Fatal("per-transmission source cap missing")
+ }
+ if got := collector.build(nil); len(got.steps) != 1 || got.steps[0].Buckets[0].Count != 1 || !got.evidence.Incomplete {
+ t.Fatal("source bound altered per-transmission dedup")
+ }
+ collector = newRegionsRoutesCollector(now)
+ collector.pathCount = maxRegionsRoutePaths
+ collector.capture(routeTransmission(2, now.Add(-time.Hour), path))
+ if len(collector.txs) != 0 || !collector.evidence.SourceTruncated {
+ t.Fatal("global path budget missing")
+ }
+ collector = newRegionsRoutesCollector(now)
+ for i := 0; i < maxRegionsRouteSteps+1; i++ {
+ to := fmt.Sprintf("%064x", i+1)
+ collector.txs = append(collector.txs, regionsRouteTx{id: i + 1, when: now.Add(-time.Hour), scope: "#sample", paths: []regionsRoutePath{{path: `["` + a + `","` + to + `"]`, observations: 1}}})
+ }
+ got := collector.build(nil)
+ if len(got.steps) != maxRegionsRouteSteps || !got.stepsTruncated || !got.evidence.Incomplete {
+ t.Fatal("step row budget missing")
+ }
+ for i, j := 0, len(collector.txs)-1; i < j; i, j = i+1, j-1 {
+ collector.txs[i], collector.txs[j] = collector.txs[j], collector.txs[i]
+ }
+ if other := collector.build(nil); !reflect.DeepEqual(got.steps, other.steps) {
+ t.Fatal("row selection depends on map/source encounter order")
+ }
+}
+
+func TestRegionRoutesActualGlobalCellBound(t *testing.T) {
+ now := time.Date(2026, 10, 16, 12, 35, 0, 0, time.UTC)
+ collector := newRegionsRoutesCollector(now)
+ a := strings.Repeat("ab", 32)
+ id := 0
+ for i := 0; i < 200; i++ {
+ path := `["` + a + `","` + fmt.Sprintf("%064x", i+1) + `"]`
+ for hour := 0; hour < 501; hour++ {
+ id++
+ collector.txs = append(collector.txs, regionsRouteTx{id: id, when: collector.start.Add(time.Duration(hour) * time.Hour), scope: "#sample", paths: []regionsRoutePath{{path: path, observations: 1}}})
+ }
+ }
+ got := collector.build(nil)
+ cells := 0
+ for _, step := range got.steps {
+ cells += len(step.Buckets)
+ }
+ if cells != maxRegionsRouteCells || !got.bucketsTruncated || !got.evidence.Incomplete {
+ t.Fatalf("cell limit=%d truncated=%v", cells, got.bucketsTruncated)
+ }
+}
+
+func TestOpenAPIRegionsRoutesSchemas(t *testing.T) {
+ if routeDescriptions()["GET /api/regions/routes"].Response["$ref"] != "#/components/schemas/RegionsRoutesResponse" {
+ t.Fatal("missing route response schema")
+ }
+ schemas := componentSchemas()
+ response := schemas["RegionsRoutesResponse"].(*openAPISchema)
+ if response.Properties["nodes"].Items.Ref != "#/components/schemas/RegionsHistoryNode" || response.Properties["stepNodes"].Items.Ref != "#/components/schemas/RegionsRouteNode" || response.Properties["steps"].Items.Ref != "#/components/schemas/RegionsRouteStep" {
+ t.Fatal("missing paired response shape")
+ }
+}
+
+func regionsRoutesBenchmarkFixture(b *testing.B) *Server {
+ srv, now, _ := regionsObservedFixture(b)
+ keys := make([]string, 2000)
+ transaction, err := srv.db.conn.Begin()
+ if err != nil {
+ b.Fatal(err)
+ }
+ stmt, err := transaction.Prepare("INSERT INTO nodes(public_key,name,role,lat,lon) VALUES(?,'Synthetic route relay','repeater',55.2,10.3)")
+ if err != nil {
+ b.Fatal(err)
+ }
+ for i := range keys {
+ keys[i] = fmt.Sprintf("%064x", i+1)
+ if _, err := stmt.Exec(keys[i]); err != nil {
+ b.Fatal(err)
+ }
+ }
+ stmt.Close()
+ if err := transaction.Commit(); err != nil {
+ b.Fatal(err)
+ }
+ end := now.Truncate(time.Hour)
+ for i := 0; i < 30000; i++ {
+ a, b, c := keys[i%len(keys)], keys[(i+1)%len(keys)], keys[(i+2)%len(keys)]
+ path := `["` + a + `","` + b + `","` + c + `"]`
+ tx := routeTransmission(i+1, end.Add(-time.Duration(i%168+1)*time.Hour), path, path)
+ tx.PathJSON = path
+ for _, key := range []string{a, b, c} {
+ srv.store.byPathHop[key] = append(srv.store.byPathHop[key], tx)
+ }
+ }
+ return srv
+}
+
+func BenchmarkRegionsRoutesCompute30K2K(b *testing.B) {
+ srv := regionsRoutesBenchmarkFixture(b)
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ _ = srv.store.computeRepeaterRelaySnapshot(24)
+ }
+}
+
+func BenchmarkRegionsRoutesWarm30K2K(b *testing.B) {
+ srv := regionsRoutesBenchmarkFixture(b)
+ recomputeRepeaterEnrichmentSafe(srv.store, 24)
+ response := getRegionsRoutes(b, srv)
+ encoded, err := json.Marshal(response)
+ if err != nil {
+ b.Fatal(err)
+ }
+ srv.store.byPathHop = nil
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ w := httptest.NewRecorder()
+ srv.handleRegionsRoutes(w, httptest.NewRequest("GET", "/api/regions/routes", nil))
+ if w.Code != 200 {
+ b.Fatal(w.Code)
+ }
+ }
+ b.ReportMetric(float64(len(encoded)), "payload-bytes")
+ b.ReportMetric(float64(len(response.Steps)), "steps")
+}
+
+func BenchmarkRegionsRoutesCapture30K2K(b *testing.B) {
+ srv := regionsRoutesBenchmarkFixture(b)
+ type evidence struct {
+ tx *StoreTx
+ when time.Time
+ }
+ items := make([]evidence, 0, 30000)
+ seen := make(map[int]bool)
+ for _, list := range srv.store.byPathHop {
+ for _, tx := range list {
+ if !seen[tx.ID] {
+ seen[tx.ID] = true
+ when, _ := parseRelayTS(tx.FirstSeen)
+ items = append(items, evidence{tx, when})
+ }
+ }
+ }
+ now := time.Now().UTC()
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ collector := newRegionsRoutesCollector(now)
+ for _, item := range items {
+ collector.captureAt(item.tx, item.when, true)
+ }
+ }
+}
+
+func BenchmarkRegionsRoutesCapPayload(b *testing.B) {
+ response := RegionsRoutesResponse{Steps: make([]RegionsRouteStep, 5000), StepNodes: []RegionsRouteNode{}}
+ for i := range response.Steps {
+ buckets := make([]RegionsHistoryBucket, 20)
+ for j := range buckets {
+ buckets[j] = RegionsHistoryBucket{Index: 480 + j, Count: 12345, LastRelayed: "2026-10-16T11:59:59.123456789Z"}
+ }
+ response.Steps[i] = RegionsRouteStep{From: fmt.Sprintf("%064x", i), To: fmt.Sprintf("%064x", i+1), Scope: "#sample", Buckets: buckets}
+ }
+ encoded, err := json.Marshal(response)
+ if err != nil {
+ b.Fatal(err)
+ }
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ if _, err := json.Marshal(response); err != nil {
+ b.Fatal(err)
+ }
+ }
+ b.ReportMetric(float64(len(encoded)), "payload-bytes")
+}
+
+func TestRegionsRoutesUnavailable(t *testing.T) {
+ srv, _, _ := regionsObservedFixture(t)
+ w := httptest.NewRecorder()
+ srv.handleRegionsRoutes(w, httptest.NewRequest("GET", "/api/regions/routes", nil))
+ if w.Code != 503 || srv.store.repeaterRelayCache != nil {
+ t.Fatal("cold routes computed inline")
+ }
+ var body struct {
+ Error string `json:"error"`
+ }
+ if json.Unmarshal(w.Body.Bytes(), &body) != nil || body.Error == "" {
+ t.Fatal("missing typed error response")
+ }
+}
diff --git a/cmd/server/repeater_enrich_bulk.go b/cmd/server/repeater_enrich_bulk.go
index ee543e2d5..c8d343c06 100644
--- a/cmd/server/repeater_enrich_bulk.go
+++ b/cmd/server/repeater_enrich_bulk.go
@@ -98,6 +98,7 @@ func (s *PacketStore) computeRepeaterRelaySnapshot(windowHours float64) *repeate
index := make(map[int]int, 1<<14)
txs := make([]bulkRelayTx, 0, 1<<14)
confirmed := make(map[string]struct{})
+ routes := newRegionsRoutesCollector(now)
add := func(list []*StoreTx) {
for _, tx := range list {
if tx == nil {
@@ -109,6 +110,7 @@ func (s *PacketStore) computeRepeaterRelaySnapshot(windowHours float64) *repeate
b := bulkRelayTx{entry: newRelayEntry(tx, false)}
b.entry.parsed = true
b.entry.t, b.entry.valid = parseRelayTS(tx.FirstSeen)
+ routes.captureAt(tx, b.entry.t, b.entry.valid)
if txHasObservedFloodPath(tx) {
forEachObservedRelayHop(tx, func(token string) bool {
key := resolve.key(token)
@@ -213,6 +215,7 @@ func (s *PacketStore) computeRepeaterRelaySnapshot(windowHours float64) *repeate
}
snapshot.info = out
snapshot.history = history.history
+ snapshot.routes = routes.build(pm)
return snapshot
}
diff --git a/cmd/server/routes.go b/cmd/server/routes.go
index d802650cb..4f32b598d 100644
--- a/cmd/server/routes.go
+++ b/cmd/server/routes.go
@@ -314,6 +314,7 @@ func (s *Server) RegisterRoutes(r *mux.Router) {
r.HandleFunc("/api/config/regions", s.handleConfigRegions).Methods("GET")
r.HandleFunc("/api/regions/observed", s.handleRegionsObserved).Methods("GET")
r.HandleFunc("/api/regions/history", s.handleRegionsHistory).Methods("GET")
+ r.HandleFunc("/api/regions/routes", s.handleRegionsRoutes).Methods("GET")
r.HandleFunc("/api/config/theme", s.handleConfigTheme).Methods("GET")
r.HandleFunc("/api/config/map", s.handleConfigMap).Methods("GET")
r.HandleFunc("/api/config/geo-filter", s.handleConfigGeoFilter).Methods("GET")
diff --git a/cmd/server/store.go b/cmd/server/store.go
index 78dcf7d19..f86ddc9c2 100644
--- a/cmd/server/store.go
+++ b/cmd/server/store.go
@@ -113,6 +113,10 @@ type StoreObs struct {
// tx.RouteType. The frame itself is not kept (#881). It fills padding
// after tsParsedOK, so StoreObs does not grow.
wireRoute uint8
+ // Own transport codes, cached without retaining a duplicate raw frame.
+ // These fields consume existing trailing padding (StoreObs stays 200 B).
+ transportKnown bool
+ wireTransport uint32
}
// ParsedTime returns the parsed Timestamp, caching the result.
@@ -1112,8 +1116,8 @@ func (s *PacketStore) Load() error {
Score: nullIntPtr(score),
PathJSON: obsPJ,
Timestamp: normalizeTimestamp(nullStrVal(obsTimestamp)),
- wireRoute: wireRouteFromRawHex(obsRawHex.String),
}
+ cacheObservationWireEvidence(obs, obsRawHex.String)
// Decode-window: feed the relay-hop indexes, don't store on struct.
s.indexObservationRelayHops(tx, decodePersistedRelayPath(nullStrVal(resolvedPathStr)), obsPJ, obsIDStr, relayPM, hopsSeen, &coldLoadAmbiguousHopsSkipped)
@@ -1430,8 +1434,8 @@ func (s *PacketStore) loadChunk(from, to time.Time) error {
Score: nullIntPtr(score),
PathJSON: obsPJ,
Timestamp: normalizeTimestamp(nullStrVal(obsTimestamp)),
- wireRoute: wireRouteFromRawHex(obsRawHex.String),
}
+ cacheObservationWireEvidence(obs, obsRawHex.String)
tx.mergeObservedPathHashSize(obsPJ)
tx.Observations = append(tx.Observations, obs)
@@ -3423,8 +3427,8 @@ func (s *PacketStore) IngestNewFromDB(sinceID, limit int) ([]map[string]interfac
Score: r.score,
PathJSON: r.pathJSON,
Timestamp: normalizeTimestamp(r.obsTS),
- wireRoute: wireRouteFromRawHex(r.obsRawHex),
}
+ cacheObservationWireEvidence(obs, r.obsRawHex)
// Relay-hop indexes: same input as Load (#158 follow-up).
// Decode-window discipline: decode once, feed consumers, never
@@ -3814,8 +3818,8 @@ func (s *PacketStore) IngestNewObservations(sinceObsID, limit int) []map[string]
// tx.RawHex and enrichObs falls back to tx.RawHex when obs.RawHex
// == "". Live-polled observations must not re-introduce the dup.
Timestamp: normalizeTimestamp(r.timestamp),
- wireRoute: wireRouteFromRawHex(r.rawHex),
}
+ cacheObservationWireEvidence(obs, r.rawHex)
// Relay-hop indexes: same input as Load (#158 follow-up).
// Decode-window discipline: decode, feed consumers, don't store on struct.
diff --git a/docs/regions-observed.md b/docs/regions-observed.md
index baec190ad..f381de963 100644
--- a/docs/regions-observed.md
+++ b/docs/regions-observed.md
@@ -131,17 +131,104 @@ payload while scrubbing. No new endpoint, persistence, retention setting,
dependency or operator configuration is introduced. Any future health
thresholds must be evidence-based and exposed through the customizer.
-## Following milestones
+## Connections and observed routes
-1. **Connections between regions:** add observed paths with direction and
- distinct-transmission counts. Preserve scope transitions, distinguish
- ambiguous hop identities and avoid interpreting shared scope membership as
- proof of a direct radio link. Route changes belong to this path-evidence
- milestone; timeline relay membership alone does not establish a changed route.
+**Connections & routes** separates two evidence classes instead of drawing
+inferred traffic links between region names:
-The default period and any future health thresholds or playback settings should
-be exposed in the customizer in a later milestone. The first milestone uses a
-24-hour default and existing theme variables, without a new operator config key.
+- **Shared relay-history memberships** counts distinct visible identities
+ attributed to both scopes by the existing transmission-level history during
+ the selected interval. This is an undirected history relationship, not
+ independently verified forwarding in both scopes. It does not prove that a
+ packet crossed between those scopes, that the identity is a gateway, or that
+ the scopes are neighbouring geographic areas.
+- **Observed directed relay steps** counts consecutive, unambiguously resolved
+ relay identities within a single observed flood path, labelled with that
+ observation's attributable packet scope. Opposite directions remain separate.
+ Repeated observers or loops contribute at most once per transmission, directed
+ step and scope. Counts across steps are step appearances, not globally unique
+ transmissions. Independent steps are never concatenated into a claimed full
+ route.
+
+Shared memberships deliberately reuse the history attribution used by Timeline
+and Health. That legacy attribution does not independently check every
+observation's route and scope: when a content transmission has mixed flood and
+direct observations, planned direct-path candidates can enter its canonical
+scope's history. Consequently shared-history counts can contain false
+memberships as well as omissions; they are not guaranteed lower bounds on
+physically shared relays. The UI discloses this provenance. Only the directed
+steps below apply the stricter own-observation frame checks introduced here.
+Repairing the older history attribution is a separate correctness task rather
+than a silent change to previous stages' metrics in this PR.
+
+Flood forwarding appends the forwarding identity to the path. Direct paths
+instead name remaining intended hops, so direct observations are not evidence
+of already-traversed steps. TRACE and ADVERT packets are excluded from this
+traffic view. Unresolved, colliding, invalid or listener-only hop identities
+break adjacency: removing an unknown intermediate hop must not manufacture
+a connection between the endpoints on either side. No origin-to-first-hop or
+last-hop-to-observer edge is inferred from generic packet metadata. Observer
+RSSI/SNR is not a measurement of the internal steps.
+
+One content transmission can have observations with different route types or
+transport codes. A named-scope step therefore requires the observation's own
+explicit transport-flood header and both transport codes matching the stored
+canonical scoped frame. Missing legacy headers and mismatched or malformed
+transport evidence are omitted rather than falling back to the transmission's
+route. This reuses the stored scope attribution; it does not independently
+authenticate the region name. The scope is a property of the observed packet,
+not a home-region assignment to either relay.
+
+The wire path does not label each hop with a region, and the reserved second
+transport code is not a destination-region identifier. Shared relays, current
+declared/default scopes and the order in which listeners reported a packet
+cannot establish a scope-change point. This milestone intentionally does not
+claim directional region-to-region crossings. Such a view needs additional
+explicit crossing evidence, not geographic or membership guesses.
+
+Connections uses the same completed UTC hours, transmission `first_seen`
+assignment and current-knowledge identity reconstruction as Timeline and
+Health. Retained-history limits remain visible. Latest remains a rolling view;
+switching to Connections does not silently turn it into observation-time
+replay. Missing coordinates do not remove a step from the evidence table;
+only usable self-reported endpoint positions can be mapped. Lines describe
+observed path order, not independently verified direct RF links or coverage.
+
+The view is shareable with `view=connections`, `window`, `scope` and `at`.
+Directed-step and shared-pair table pages use `page` and `pairsPage` respectively.
+Connections has no automatic playback or route-comparison control. It can use
+the paired history payload when switching to Health or Timeline, without
+making a request for every selected hour.
+
+Route evidence is published with the existing background relay snapshot, not
+recomputed by scanning packets for each request or slider movement. The bulk
+response contains bounded hourly step counts as well as relay history. Node,
+scope, step and populated-hour limits must be disclosed as incomplete evidence.
+No database writes, schema changes, retention extension, dependencies or new
+operator configuration are required.
+
+The route collector retains at most 250,000 path variants globally and 128 per
+transmission, with an 8,192-byte input-path safety bound. Publication retains
+at most 5,000 directed step/scope rows, 100,000 populated hourly cells and 10,000
+endpoint identities. Endpoint metadata is independent of relay-history
+membership, so a valid flood observation of a canonically direct transmission
+can still be shown. The UI retains at most 1,000 shared scope pairs, paginates
+both tables in 25-row pages and maps at most 200 positioned step rows. A capped
+subset is not a ranking of all possible relations. Snapshot-wide skipped-path,
+unresolved-hop and missing-endpoint diagnostics are not selected-interval
+traffic totals. Complete retained history does not imply complete radio
+reception or that all paths can be attributed.
+
+Future work can add independently evidenced scope transitions and comparisons
+of observed routes, and align the legacy history's mixed-route/scope attribution
+with own-observation evidence. A change in timeline relay membership alone does
+not establish a changed route.
+
+The default period, display limits and any future health thresholds or playback
+settings should be exposed in the customizer in a later milestone. Protocol
+validity checks are not user-adjustable tuning parameters. The first milestone
+uses a 24-hour default and existing theme variables, without a new operator
+config key.
## Verification
@@ -167,3 +254,14 @@ UTC boundaries and no sparse-cell rescans after activity indexing. Browser
checks include Health deep links, local filter/time changes, Health/Timeline
transitions, missing-data charts, exact values, themes, teardown and narrow
mobile/desktop layouts.
+
+Connections tests additionally cover per-observation route/transport evidence,
+all observation-loading paths, unchanged observation struct size, separate
+observation boundaries, ambiguous intermediate hops, repeated/looped steps,
+reverse directions, live visibility, archived metadata and source/publication
+limits. Shared-scope relation keys must remain distinct even when region names
+contain delimiter characters. Browser validation covers direction labels,
+missing positions, partial history, local hour/scope selection without extra
+requests, keyed and bounded rendering, failures, navigation, themes and narrow
+screens. Benchmarks include a 30,000-transmission/2,000-node route workload
+and maximum frontend fanout, not only empty snapshots.
diff --git a/public/index.html b/public/index.html
index 581d350ee..245757e53 100644
--- a/public/index.html
+++ b/public/index.html
@@ -215,6 +215,7 @@
+
diff --git a/public/regions-history.js b/public/regions-history.js
index 4e09845bc..aa2d93e68 100644
--- a/public/regions-history.js
+++ b/public/regions-history.js
@@ -183,7 +183,7 @@
ratePerHour: frame.currentCoverage === 'complete' && !history.limited ? total / hours : null };
}
- const api = { indexHistory, frameIndex, frameTime, interval, selectFrame, indexActivity, selectActivity };
+ const api = { indexHistory, frameIndex, frameTime, interval, selectFrame, scopeCount, indexActivity, selectActivity };
if (typeof module !== 'undefined' && module.exports) module.exports = api;
root.RegionsHistory = api;
})(typeof window === 'undefined' ? globalThis : window);
diff --git a/public/regions-map.js b/public/regions-map.js
index 3b25457c4..210019507 100644
--- a/public/regions-map.js
+++ b/public/regions-map.js
@@ -90,6 +90,11 @@
return scopeCatalog(nodes).scopes;
}
+ function mergeScopeCatalog(catalog, additionalScopes) {
+ const names = Array.from(new Set(catalog.scopes.concat(additionalScopes.filter(validScope)))).sort((a, b) => a.localeCompare(b));
+ return { scopes: names.slice(0, MAX_SCOPES), truncated: catalog.truncated || names.length > MAX_SCOPES };
+ }
+
function convexHull(points) {
if (!Array.isArray(points)) return [];
const sorted = points.filter(p => Array.isArray(p) && p.length === 2 && Number.isFinite(p[0]) && Number.isFinite(p[1]))
@@ -118,10 +123,16 @@
const scope = params.get('scope') || '';
const window = params.get('window');
const selection = { window: Object.prototype.hasOwnProperty.call(WINDOWS, window) ? window : '24h', scope: validScope(scope) ? scope : '' };
- if (params.get('view') === 'timeline' || params.get('view') === 'health') {
+ if (['timeline', 'health', 'connections'].includes(params.get('view'))) {
selection.view = params.get('view');
selection.at = isoTime(params.get('at'));
- selection.compare = params.get('compare') === '1';
+ selection.compare = selection.view !== 'connections' && params.get('compare') === '1';
+ if (selection.view === 'connections') {
+ for (const key of ['page', 'pairsPage']) {
+ const value = Number(params.get(key));
+ if (Number.isInteger(value) && value > 1 && value <= 200) selection[key] = value;
+ }
+ }
}
return selection;
}
@@ -133,11 +144,14 @@
function scopeHash(scope, window = '24h', selection = {}) {
const params = new URLSearchParams({ window: Object.prototype.hasOwnProperty.call(WINDOWS, window) ? window : '24h' });
if (validScope(scope)) params.set('scope', scope);
- if (selection.view === 'timeline' || selection.view === 'health') {
+ if (['timeline', 'health', 'connections'].includes(selection.view)) {
params.set('view', selection.view);
const at = isoTime(selection.at);
if (at) params.set('at', at);
- if (selection.compare) params.set('compare', '1');
+ if (selection.view !== 'connections' && selection.compare) params.set('compare', '1');
+ if (selection.view === 'connections') for (const key of ['page', 'pairsPage']) {
+ if (Number.isInteger(selection[key]) && selection[key] > 1 && selection[key] <= 200) params.set(key, selection[key]);
+ }
}
return '#/regions?' + params.toString();
}
@@ -360,7 +374,7 @@
const viewLabel = element('label', '', 'View ');
const view = element('select');
view.setAttribute('aria-label', 'Region view');
- view.append(new Option('Latest', 'latest'), new Option('Timeline', 'timeline'), new Option('Health & trends', 'health'));
+ view.append(new Option('Latest', 'latest'), new Option('Timeline', 'timeline'), new Option('Health & trends', 'health'), new Option('Connections', 'connections'));
view.value = selection.view || 'latest';
viewLabel.appendChild(view);
controls.appendChild(viewLabel);
@@ -384,7 +398,7 @@
controls.appendChild(fitButton);
shell.appendChild(controls);
const timelineControls = element('div', 'regions-timeline');
- timelineControls.hidden = selection.view !== 'timeline' && selection.view !== 'health';
+ timelineControls.hidden = !['timeline', 'health', 'connections'].includes(selection.view);
const transport = element('div', 'regions-transport');
function transportButton(text, label) {
const button = element('button', '', text);
@@ -427,6 +441,11 @@
shell.appendChild(coverage);
const activityPanel = createActivityPanel();
shell.appendChild(activityPanel.panel);
+ const routesTools = root.RegionsRoutes;
+ const routesPanel = routesTools ? routesTools.createPanel((key, value) => {
+ selection[key] = value; saveSelection(); draw(false);
+ }) : null;
+ if (routesPanel) shell.appendChild(routesPanel.panel);
const details = element('details', 'regions-details');
details.appendChild(element('summary', '', 'Data & position details'));
details.appendChild(element('p', '', 'Repeaters and rooms are shown where scoped relay traffic was observed. Positions are self-reported; their accuracy is unverified. Nodes without usable positions are counted but not mapped. Dashed outlines join the outermost node positions and do not measure RF coverage or administrative boundaries.'));
@@ -458,9 +477,9 @@
if (redrawMap) redrawMap();
};
let tileLayer = null;
- function ensureMap() {
+ function ensureMap(reportError = true) {
if (map) return true;
- if (typeof L === 'undefined') { status.textContent = 'Map library unavailable.'; return false; }
+ if (typeof L === 'undefined') { if (reportError) status.textContent = 'Map library unavailable.'; return false; }
map = L.map(mapEl, { zoomAnimation: false }).setView([55.7, 10.5], 6);
leafletMap = map;
layer = L.layerGroup().addTo(map);
@@ -472,20 +491,25 @@
let requestGeneration = 0, snapshot = null, nodes = [], catalog = { scopes: [], truncated: false };
let indexedHistory = null, timer = null, lastFrame = null, scrubFrame = null;
let activity = null, activityScope = null;
+ let routesIndex = null;
const markers = new Map();
let hullLayer = null;
const historyTools = root.RegionsHistory;
- function historicalView() { return selection.view === 'timeline' || selection.view === 'health'; }
+ function historicalView() { return ['timeline', 'health', 'connections'].includes(selection.view); }
function updateView() {
const health = selection.view === 'health';
+ const connections = selection.view === 'connections';
timelineControls.hidden = !historicalView();
- play.hidden = health;
+ play.hidden = health || connections;
+ compareLabel.hidden = connections;
fitButton.hidden = health;
mapEl.hidden = health;
shell.classList.toggle('regions-page-health', health);
+ shell.classList.toggle('regions-page-connections', connections);
if (!health) activityPanel.clear();
+ if (!connections && routesPanel) routesPanel.clear();
if (health) shell.insertBefore(comparison, details); else shell.insertBefore(comparison, status);
}
@@ -654,7 +678,20 @@
coverage.setAttribute('data-coverage-state', frame.currentCoverage);
exactBounds.replaceChildren(element('p', '', 'Selected interval (UTC): ' + frame.interval.start + ' — ' + frame.interval.end),
element('p', '', 'History dataset (UTC): ' + new Date(indexedHistory.start).toISOString() + ' — ' + new Date(indexedHistory.end).toISOString()));
- if (selection.view === 'health') {
+ if (selection.view === 'connections') {
+ if (!routesIndex || !routesPanel) throw new Error('Connection evidence unavailable');
+ const connections = routesTools.selectRoutes(routesIndex, indexedHistory, frame, selection);
+ const pages = routesPanel.update(connections, frame, selection);
+ selection.page = pages.page; selection.pairsPage = pages.pairsPage;
+ saveSelection(true);
+ comparison.hidden = true;
+ status.textContent = connections.steps.length + ' directed step/scope rows and ' + connections.pairs.length +
+ (connections.pairsTruncated ? '+ retained' : '') + ' shared-history scope pairs.' +
+ (connections.complete ? '' : ' Evidence is incomplete or capped; absence cannot be established.');
+ if (catalog.truncated) status.textContent += ' Scope selector is limited to 128 names; a selected scope outside the list is preserved.';
+ if (ensureMap(false)) routesPanel.renderMap(map, layer, L);
+ else status.textContent += ' Map library unavailable; the evidence tables remain available.';
+ } else if (selection.view === 'health') {
if (!activity || activityScope !== selection.scope) {
activity = historyTools.indexActivity(indexedHistory, selection.scope);
activityScope = selection.scope;
@@ -666,7 +703,7 @@
'No scoped relay activity observed in this complete interval; this does not establish an outage.') :
'Retained observations only; history is incomplete or limited.');
} else renderTimelinePoints(frame);
- renderComparison(frame);
+ if (selection.view !== 'connections') renderComparison(frame);
}
function populateScopes(loading) {
@@ -674,7 +711,8 @@
select.appendChild(new Option('All observed scopes', ''));
for (const scope of catalog.scopes) select.appendChild(new Option(scope, scope));
if (selection.scope && !catalog.scopes.includes(selection.scope)) {
- const hasSelectedData = nodes.some(node => node.scopes.some(s => s.name === selection.scope));
+ const hasSelectedData = nodes.some(node => node.scopes.some(s => s.name === selection.scope)) ||
+ (routesIndex && routesIndex.steps.some(step => step.scope === selection.scope));
select.appendChild(new Option((loading ? 'Selected scope: ' : hasSelectedData ? 'Selected (outside list): ' : 'No data in interval: ') + selection.scope, selection.scope));
}
select.value = selection.scope;
@@ -687,6 +725,7 @@
if (selection.view === 'timeline' && !ensureMap()) return;
drawTimeline();
if (selection.view === 'timeline' && fit) fitTimeline();
+ if (selection.view === 'connections' && fit && map) routesPanel.fitMap(map, L);
return;
}
if (!snapshot) return;
@@ -694,7 +733,11 @@
renderPoints(layer, map, nodes, selection.scope, status, snapshot.truncated || snapshot.nodes.length > MAX_POINTS,
snapshot.scopesTruncated || catalog.truncated, fit);
}
- redrawMap = () => { if (selection.view === 'timeline' && indexedHistory && lastFrame) renderTimelinePoints(lastFrame); };
+ redrawMap = () => {
+ if (!indexedHistory || !lastFrame) return;
+ if (selection.view === 'timeline') renderTimelinePoints(lastFrame);
+ if (selection.view === 'connections' && routesPanel && map) routesPanel.renderMap(map, layer, L);
+ };
async function load() {
pause();
@@ -705,6 +748,8 @@
indexedHistory = null;
activity = null; activityScope = null;
activityPanel.clear();
+ routesIndex = null;
+ if (routesPanel) routesPanel.clear();
nodes = [];
catalog = { scopes: [], truncated: false };
clearPoints();
@@ -721,7 +766,7 @@
exactBounds.replaceChildren();
coverage.removeAttribute('data-coverage-state');
try {
- const data = await api(historical ? '/regions/history' : '/regions/observed?window=' + encodeURIComponent(window), { ttl: 0, bust: true, retry503: false });
+ const data = await api(selection.view === 'connections' ? '/regions/routes' : historical ? '/regions/history' : '/regions/observed?window=' + encodeURIComponent(window), { ttl: 0, bust: true, retry503: false });
if (token !== generation || request !== requestGeneration || leafletMap !== map) return;
if (historical) {
if (!historyTools || !data || !Array.isArray(data.nodes)) throw new Error('Invalid regions history');
@@ -732,6 +777,12 @@
}, nodes);
catalog = scopeCatalog(nodes);
if (catalog.truncated) indexedHistory.limited = true;
+ if (selection.view === 'connections') {
+ if (!routesTools || !routesPanel) throw new Error('Connection module unavailable');
+ routesIndex = routesTools.indexRoutes(data, indexedHistory, validScope, validPosition);
+ catalog = mergeScopeCatalog(catalog, routesIndex.steps.map(step => step.scope));
+ if (catalog.truncated) routesIndex.limited = true;
+ }
populateScopes(false);
draw(true);
saveSelection(true);
@@ -784,13 +835,18 @@
view.addEventListener('change', () => {
pause();
const previousHistorical = historicalView();
+ const previousView = selection.view;
selection.view = view.value;
+ if (previousView !== selection.view && (previousView === 'connections' || selection.view === 'connections')) clearPoints();
+ if (selection.view === 'connections') { selection.compare = false; compare.checked = false; }
if (!historicalView()) { selection.at = ''; selection.compare = false; compare.checked = false; }
updateView();
+ // Health leaves an existing map hidden and skips tile updates. Refresh
+ // both its theme and dimensions before fitting any visible map view.
+ if (map && selection.view !== 'health') { applyTiles(); map.invalidateSize(); }
saveSelection();
- if (previousHistorical && historicalView() && indexedHistory) {
- draw(selection.view === 'timeline');
- if (map && selection.view === 'timeline') { applyTiles(); map.invalidateSize(); }
+ if (previousHistorical && historicalView() && indexedHistory && (selection.view !== 'connections' || routesIndex)) {
+ draw(selection.view === 'timeline' || selection.view === 'connections');
} else load();
});
compare.addEventListener('change', () => {
@@ -831,7 +887,8 @@
timer = setTimeout(tick, 1000);
});
fitButton.addEventListener('click', () => {
- if (selection.view === 'timeline') { if (indexedHistory) fitTimeline(); }
+ if (selection.view === 'connections') { if (map && routesPanel) routesPanel.fitMap(map, L); }
+ else if (selection.view === 'timeline') { if (indexedHistory) fitTimeline(); }
else draw(true);
});
refresh.addEventListener('click', load);
@@ -852,7 +909,7 @@
}
const internals = { validPosition, validScope, collectScopeNodes, collectScopePoints, selectionSummary,
- lastRelayedForScope, listScopes, scopeCatalog, convexHull, parseSelection, parseSelectedScope, scopeHash, coverageInfo };
+ lastRelayedForScope, listScopes, scopeCatalog, mergeScopeCatalog, convexHull, parseSelection, parseSelectedScope, scopeHash, coverageInfo };
if (typeof module !== 'undefined' && module.exports) module.exports = internals;
if (typeof root.registerPage === 'function') root.registerPage('regions', { init, destroy });
})(typeof window === 'undefined' ? globalThis : window);
diff --git a/public/regions-routes.js b/public/regions-routes.js
new file mode 100644
index 000000000..6bcdfe9a4
--- /dev/null
+++ b/public/regions-routes.js
@@ -0,0 +1,296 @@
+/* Connections retain two separate facts: shared relay-history attribution and directed
+ * adjacent steps from scope-matched flood observations. Neither is a handoff. */
+'use strict';
+(function (root) {
+ const historyTools = typeof module !== 'undefined' && module.exports ? require('./regions-history.js') : root.RegionsHistory;
+ const MAX_STEPS = 5000, MAX_CELLS = 100000, MAX_PAIRS = 1000, PAGE_SIZE = 25, MAP_LIMIT = 200;
+ const keyPattern = /^[0-9a-f]{64}$/i;
+
+ function indexRoutes(data, history, validScope, validPosition) {
+ const nodes = new Map(history.nodes.map(node => [node.publicKey.toLowerCase(), node]));
+ let limited = !!(data.stepsTruncated || data.stepBucketsTruncated || data.stepEvidence?.sourceTruncated || data.stepEvidence?.incomplete);
+ for (const key of ['skippedObservations', 'unresolvedHops', 'missingEndpointSteps']) {
+ const count = data.stepEvidence?.[key];
+ if (count != null && (!Number.isSafeInteger(count) || count < 0 || count > 0)) limited = true;
+ }
+ if (Array.isArray(data.stepNodes)) {
+ if (data.stepNodes.length > MAX_STEPS * 2) limited = true;
+ for (const node of data.stepNodes.slice(0, MAX_STEPS * 2)) {
+ if (!node || !keyPattern.test(node.publicKey) || !['repeater', 'room'].includes(node.role)) { limited = true; continue; }
+ const positioned = node.positionSource === 'reported' && typeof validPosition === 'function' && validPosition(node);
+ nodes.set(node.publicKey.toLowerCase(), { publicKey: node.publicKey.toLowerCase(), name: String(node.name || node.publicKey.slice(0, 8)), role: node.role,
+ positionSource: positioned ? 'reported' : 'none', lat: positioned ? Number(node.lat) : null, lon: positioned ? Number(node.lon) : null,
+ metadataSource: node.metadataSource === 'archived' ? 'archived' : 'current' });
+ }
+ }
+ const input = Array.isArray(data.steps) ? data.steps : [];
+ if (!Array.isArray(data.steps) || input.length > MAX_STEPS) limited = true;
+ const steps = [], seen = new Set();
+ let cells = 0, total = 0;
+ for (const source of input.slice(0, MAX_STEPS)) {
+ if (!source || !keyPattern.test(source.from) || !keyPattern.test(source.to) ||
+ !validScope(source.scope) || source.from.toLowerCase() === source.to.toLowerCase()) { limited = true; continue; }
+ const from = source.from.toLowerCase(), to = source.to.toLowerCase();
+ const key = JSON.stringify([from, to, source.scope]);
+ if (!nodes.has(from) || !nodes.has(to) || seen.has(key) || !Array.isArray(source.buckets)) { limited = true; continue; }
+ seen.add(key);
+ const values = [];
+ if (source.buckets.length > 504) limited = true;
+ for (const bucket of source.buckets.slice(0, 504)) {
+ if (++cells > MAX_CELLS) { limited = true; break; }
+ const last = typeof bucket?.lastRelayed === 'string' ? Date.parse(bucket.lastRelayed) : NaN;
+ if (!Number.isInteger(bucket?.index) || bucket.index < 0 || bucket.index >= 504 ||
+ !Number.isSafeInteger(bucket.count) || bucket.count < 1 || !Number.isSafeInteger(total + bucket.count) ||
+ !Number.isFinite(last) || last < history.start + bucket.index * 3600000 || last >= history.start + (bucket.index + 1) * 3600000) {
+ limited = true; continue;
+ }
+ total += bucket.count;
+ values.push({ index: bucket.index, count: bucket.count, time: new Date(last).toISOString() });
+ }
+ values.sort((a, b) => a.index - b.index);
+ const indices = [], prefix = [0], times = [];
+ for (const bucket of values) {
+ if (indices[indices.length - 1] === bucket.index) { limited = true; continue; }
+ indices.push(bucket.index); prefix.push(prefix[prefix.length - 1] + bucket.count); times.push(bucket.time);
+ }
+ if (indices.length) steps.push({ key, from, to, scope: source.scope, fromNode: nodes.get(from), toNode: nodes.get(to), indices, prefix, times });
+ }
+ const evidence = data.stepEvidence || {};
+ const count = value => Number.isSafeInteger(value) && value >= 0 ? value : 0;
+ return { history, steps, limited, evidence: { skippedObservations: count(evidence.skippedObservations), unresolvedHops: count(evidence.unresolvedHops), missingEndpointSteps: count(evidence.missingEndpointSteps),
+ sourceTruncated: evidence.sourceTruncated === true, incomplete: evidence.incomplete === true }, cached: null };
+ }
+
+ function sharedPairs(history, first, last, scope) {
+ const memberships = new Map(), groups = new Map();
+ let sharedRelays = 0;
+ history.nodes.forEach((node, index) => {
+ const active = node.scopes.filter(item => historyTools.scopeCount(item, first, last).count > 0).map(item => item.name).sort();
+ if (active.length < 2 || (scope && !active.includes(scope))) return;
+ sharedRelays++;
+ for (const name of active) {
+ if (!memberships.has(name)) memberships.set(name, []);
+ memberships.get(name).push(index);
+ }
+ const key = JSON.stringify(active);
+ if (!groups.has(key)) groups.set(key, active);
+ });
+ const candidates = new Map();
+ const possible = scope ? Math.max(0, memberships.size - 1) : memberships.size * (memberships.size - 1) / 2;
+ let truncated = false;
+ outer: for (const active of groups.values()) {
+ for (let a = 0; a < active.length; a++) for (let b = a + 1; b < active.length; b++) {
+ if (scope && active[a] !== scope && active[b] !== scope) continue;
+ const key = JSON.stringify([active[a], active[b]]);
+ if (!candidates.has(key)) {
+ if (candidates.size === MAX_PAIRS) { truncated = true; break outer; }
+ candidates.set(key, { key, from: active[a], to: active[b] });
+ }
+ }
+ if (candidates.size === possible) break;
+ }
+ const pairs = [];
+ for (const pair of candidates.values()) {
+ const left = memberships.get(pair.from), right = memberships.get(pair.to);
+ let a = 0, b = 0, count = 0;
+ const examples = [];
+ while (a < left.length && b < right.length) {
+ if (left[a] < right[b]) a++;
+ else if (right[b] < left[a]) b++;
+ else {
+ count++;
+ if (examples.length < 3) examples.push(history.nodes[left[a]]);
+ a++; b++;
+ }
+ }
+ pairs.push({ ...pair, count, examples });
+ }
+ pairs.sort((a, b) => b.count - a.count || a.key.localeCompare(b.key));
+ return { pairs, sharedRelays, pairsTruncated: truncated };
+ }
+
+ function positioned(node) { return node.positionSource === 'reported' && Number.isFinite(node.lat) && Number.isFinite(node.lon); }
+
+ function selectRoutes(index, history, frame, selection) {
+ if (index.history !== history) throw new Error('Routes index belongs to another history snapshot');
+ const key = JSON.stringify([frame.interval.start, frame.interval.end, selection.scope || '']);
+ if (index.cached?.key === key) return index.cached.value;
+ const first = (Date.parse(frame.interval.start) - history.start) / 3600000;
+ const last = (Date.parse(frame.interval.end) - history.start) / 3600000;
+ const steps = [];
+ let stepAppearances = 0, unmappedSteps = 0;
+ for (const step of index.steps) {
+ if (selection.scope && step.scope !== selection.scope) continue;
+ const value = historyTools.scopeCount(step, first, last);
+ if (!value.count) continue;
+ stepAppearances += value.count;
+ if (!positioned(step.fromNode) || !positioned(step.toNode)) unmappedSteps++;
+ steps.push({ ...step, count: value.count, lastRelayed: value.lastRelayed });
+ }
+ steps.sort((a, b) => b.count - a.count || a.key.localeCompare(b.key));
+ const pairs = sharedPairs(history, first, last, selection.scope || '');
+ const value = { ...pairs, steps, stepAppearances, unmappedSteps,
+ complete: frame.currentCoverage === 'complete' && !history.limited && !index.limited,
+ sharedComplete: frame.currentCoverage === 'complete' && !history.limited,
+ evidence: index.evidence, limited: index.limited || history.limited,
+ mapSteps: steps.filter(step => positioned(step.fromNode) && positioned(step.toNode)).slice(0, MAP_LIMIT) };
+ index.cached = { key, value };
+ return value;
+ }
+
+ let panelSequence = 0;
+ function createPanel(onPage) {
+ const el = (tag, className, text) => {
+ const node = document.createElement(tag);
+ if (className) node.className = className;
+ if (text != null) node.textContent = text;
+ return node;
+ };
+ const panel = el('section', 'regions-connections'); panel.hidden = true;
+ panel.setAttribute('aria-label', 'Scope connections and observed relay steps');
+ const summary = el('p', 'regions-connections-summary'); panel.appendChild(summary);
+ panel.appendChild(el('h2', '', 'Shared relay-history memberships'));
+ panel.appendChild(el('p', 'regions-connections-provenance', 'Distinct visible identities attributed to both scopes by the transmission-level relay history used by Timeline/Health. Observation route/scope variants are not independently verified here; mixed flood/direct transmissions can include planned-path candidates. Only directed steps below use each observation’s own frame evidence.'));
+ panel.appendChild(el('p', 'regions-connections-note', 'Shared history membership does not establish cross-scope traffic, a scope transition or a direct radio link.'));
+ const pairSummary = el('p', 'regions-pairs-summary'); panel.appendChild(pairSummary);
+
+ function tableSection(headers, className, pageKey, label) {
+ const section = el('div', 'regions-relations-section');
+ const scroll = el('div', 'regions-relations-scroll'); scroll.tabIndex = 0;
+ scroll.setAttribute('role', 'region'); scroll.setAttribute('aria-label', label);
+ const table = el('table', className), head = el('thead'), tr = el('tr');
+ for (const text of headers) { const th = el('th', '', text); th.scope = 'col'; tr.appendChild(th); }
+ head.appendChild(tr); const body = el('tbody'); table.append(head, body); scroll.appendChild(table); section.appendChild(scroll);
+ const pager = el('div', 'regions-relations-pagination');
+ const previous = el('button', '', 'Previous'), next = el('button', '', 'Next'), text = el('span');
+ previous.type = next.type = 'button'; previous.setAttribute('aria-label', 'Previous ' + label + ' page'); next.setAttribute('aria-label', 'Next ' + label + ' page');
+ let page = 1;
+ previous.addEventListener('click', () => onPage(pageKey, page - 1)); next.addEventListener('click', () => onPage(pageKey, page + 1));
+ pager.append(previous, text, next); section.appendChild(pager); panel.appendChild(section);
+ const records = new Map();
+ return { update(values, requestedPage, makeCells, signature) {
+ const pages = Math.max(1, Math.ceil(values.length / PAGE_SIZE));
+ page = Math.max(1, Math.min(pages, Number.isInteger(requestedPage) ? requestedPage : 1));
+ const selected = values.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE), keys = new Set(selected.map(value => value.key));
+ for (const [key, record] of records) if (!keys.has(key)) { record.row.remove(); records.delete(key); }
+ let cursor = body.firstChild;
+ for (const value of selected) {
+ let record = records.get(value.key);
+ if (!record) { record = { row: el('tr'), signature: null }; record.row.dataset.key = value.key; records.set(value.key, record); }
+ const current = signature(value);
+ if (current !== record.signature) { record.row.replaceChildren(...makeCells(value)); record.signature = current; }
+ if (record.row !== cursor) body.insertBefore(record.row, cursor);
+ cursor = record.row.nextSibling;
+ }
+ text.textContent = 'Page ' + page + ' of ' + pages + ' · ' + values.length + ' returned rows';
+ previous.disabled = page <= 1; next.disabled = page >= pages;
+ section.dataset.page = String(page);
+ return page;
+ } };
+ }
+
+ const pairTable = tableSection(['Scope pair', 'History identities', 'Example identities'], 'regions-pairs-table', 'pairsPage', 'shared scope pairs');
+ panel.appendChild(el('h2', '', 'Observed directed relay steps'));
+ panel.appendChild(el('p', 'regions-connections-note', 'Consecutive identity-resolved hops from scope-matched flood observations. Opposite directions are separate. Steps from different observations are never joined into a claimed full route. Counts are distinct transmissions per directed step and scope, not a global packet total.'));
+ const stepSummary = el('p', 'regions-steps-summary'); panel.appendChild(stepSummary);
+ const stepTable = tableSection(['Direction', 'Packet scope', 'Distinct transmissions', 'Last observed (UTC)'], 'regions-steps-table', 'page', 'directed relay steps');
+ const diagnostics = el('p', 'regions-step-diagnostics'); panel.appendChild(diagnostics);
+ const mapSummary = el('p', 'regions-connections-map-note'); panel.appendChild(mapSummary);
+ const link = node => { const a = el('a', '', node.name); a.href = '#/nodes/' + encodeURIComponent(node.publicKey); return a; };
+ let latest = null, attachedLayer = null, parentLayer = null, markerDefinition = null;
+ const mapRecords = new Map(), mapNodes = new Map();
+ const arrowId = 'regions-step-arrow-' + (++panelSequence);
+
+ function update(value, frame, selection) {
+ latest = value; panel.hidden = false;
+ const limited = value.complete ? '' : ' Retained evidence is incomplete or limited; missing results do not establish absence.';
+ summary.textContent = 'History memberships and observed directed steps in ' + frame.interval.start + ' — ' + frame.interval.end + '.' + limited;
+ pairSummary.textContent = value.pairs.length + (value.pairsTruncated ? '+ retained' : '') + ' scope pairs; ' + value.sharedRelays +
+ ' distinct identities with shared history membership.' + (value.sharedComplete ? '' : ' Shared membership history is incomplete or limited.') +
+ (value.pairsTruncated ? ' The 1,000-pair safety limit was reached; this is a deterministic retained subset, not a ranking of all pairs. Select a scope to narrow it.' : '');
+ const pairsPage = pairTable.update(value.pairs, selection.pairsPage, pair => {
+ const names = el('td', '', pair.from + ' — ' + pair.to), count = el('td', '', pair.count.toLocaleString()), examples = el('td');
+ pair.examples.forEach((node, index) => { if (index) examples.append(', '); examples.appendChild(link(node)); });
+ if (pair.count > pair.examples.length) examples.append(' (examples only)');
+ return [names, count, examples];
+ }, pair => JSON.stringify([pair.count, pair.examples.map(node => [node.publicKey, node.name])]));
+ stepSummary.textContent = value.steps.length + ' directed step/scope rows; ' + value.stepAppearances.toLocaleString() +
+ ' step appearances in retained observations. ' + value.unmappedSteps + ' rows have endpoints without usable positions.' +
+ (!value.steps.length && value.complete ? ' No qualifying directed steps were observed; this does not establish an outage or lack of connectivity.' : '');
+ const page = stepTable.update(value.steps, selection.page, step => {
+ const direction = el('td'); direction.append(link(step.fromNode), ' → ', link(step.toNode));
+ return [direction, el('td', '', step.scope), el('td', '', step.count.toLocaleString()), el('td', '', step.lastRelayed)];
+ }, step => JSON.stringify([step.count, step.lastRelayed, step.fromNode.name, step.toNode.name]));
+ const evidence = value.evidence;
+ diagnostics.textContent = 'Snapshot-wide evidence diagnostics (not selected-interval totals): ' + evidence.skippedObservations +
+ ' excluded or unusable observations; ' + evidence.unresolvedHops + ' unresolved hops; ' + evidence.missingEndpointSteps +
+ ' steps without publishable endpoint metadata.' + (evidence.incomplete || evidence.sourceTruncated ? ' The source snapshot is incomplete or capped.' : '') +
+ ' Direct planned paths, TRACE/adverts and mismatched or unknown scopes are excluded. Unresolved or hidden hops break adjacency.';
+ mapSummary.textContent = value.mapSteps.length + ' directed step rows mapped of ' + (value.steps.length - value.unmappedSteps) +
+ ' with usable endpoint positions. The map is limited to ' + MAP_LIMIT + ' rows. Positions are current or last-known self-reported coordinates, not historical movement or measured radio paths.';
+ panel.setAttribute('data-ready', 'true'); panel.setAttribute('data-complete', String(value.complete));
+ return { page, pairsPage };
+ }
+
+ function clearMap() {
+ if (attachedLayer) attachedLayer.clearLayers();
+ mapRecords.clear(); mapNodes.clear();
+ if (markerDefinition) markerDefinition.remove(); markerDefinition = null;
+ }
+
+ function renderMap(map, layer, leaflet) {
+ if (!latest) return;
+ if (!attachedLayer || parentLayer !== layer) { clearMap(); attachedLayer = leaflet.layerGroup(); parentLayer = layer; }
+ if (!layer.hasLayer(attachedLayer)) layer.addLayer(attachedLayer);
+ const accent = getComputedStyle(document.documentElement).getPropertyValue('--accent').trim();
+ const wanted = new Set(), nodes = new Map();
+ for (const step of latest.mapSteps) {
+ wanted.add(step.key); nodes.set(step.from, step.fromNode); nodes.set(step.to, step.toNode);
+ let record = mapRecords.get(step.key);
+ if (!record) {
+ record = { line: leaflet.polyline([[step.fromNode.lat, step.fromNode.lon], [step.toNode.lat, step.toNode.lon]], {
+ color: accent, weight: 3, opacity: 0.8, className: 'regions-directed-step',
+ }).addTo(attachedLayer), signature: '' };
+ mapRecords.set(step.key, record);
+ }
+ record.line.setStyle({ color: accent });
+ const signature = step.count + '|' + step.lastRelayed;
+ if (record.signature !== signature) {
+ const popup = el('div'); popup.append(link(step.fromNode), ' → ', link(step.toNode),
+ el('div', '', step.count + ' distinct transmissions · ' + step.scope), el('div', '', 'Last observed: ' + step.lastRelayed));
+ record.line.bindPopup(popup); record.signature = signature;
+ }
+ const path = record.line.getElement();
+ if (path) { path.setAttribute('marker-end', 'url(#' + arrowId + ')'); path.setAttribute('role', 'img'); path.setAttribute('aria-label', step.fromNode.name + ' to ' + step.toNode.name + ', ' + step.count + ' distinct transmissions'); }
+ }
+ for (const [key, record] of mapRecords) if (!wanted.has(key)) { attachedLayer.removeLayer(record.line); mapRecords.delete(key); }
+ for (const [key, marker] of mapNodes) if (!nodes.has(key)) { attachedLayer.removeLayer(marker); mapNodes.delete(key); }
+ for (const [key, node] of nodes) if (!mapNodes.has(key)) {
+ mapNodes.set(key, leaflet.circleMarker([node.lat, node.lon], { radius: 5, color: accent, fillOpacity: 0.8 }).bindPopup(link(node)).addTo(attachedLayer));
+ } else mapNodes.get(key).setStyle({ color: accent });
+ if (!markerDefinition && mapRecords.size) {
+ const svg = attachedLayer.getLayers()[0]?.getElement()?.ownerSVGElement;
+ if (svg) {
+ markerDefinition = document.createElementNS('http://www.w3.org/2000/svg', 'defs');
+ const marker = document.createElementNS('http://www.w3.org/2000/svg', 'marker');
+ marker.id = arrowId; marker.setAttribute('viewBox', '0 0 10 10'); marker.setAttribute('refX', '9'); marker.setAttribute('refY', '5');
+ marker.setAttribute('markerWidth', '5'); marker.setAttribute('markerHeight', '5'); marker.setAttribute('orient', 'auto');
+ const arrow = document.createElementNS('http://www.w3.org/2000/svg', 'path'); arrow.setAttribute('d', 'M 0 0 L 10 5 L 0 10 z'); arrow.setAttribute('class', 'regions-step-arrow');
+ marker.appendChild(arrow); markerDefinition.appendChild(marker); svg.prepend(markerDefinition);
+ }
+ }
+ }
+
+ function fitMap(map, leaflet) {
+ if (!latest?.mapSteps.length) return;
+ const points = latest.mapSteps.flatMap(step => [[step.fromNode.lat, step.fromNode.lon], [step.toNode.lat, step.toNode.lon]]);
+ map.fitBounds(leaflet.latLngBounds(points).pad(0.15), { maxZoom: 10 });
+ }
+ function clear() { panel.hidden = true; panel.removeAttribute('data-ready'); clearMap(); }
+ return { panel, update, renderMap, fitMap, clear, clearMap };
+ }
+
+ const api = { indexRoutes, selectRoutes, createPanel, PAGE_SIZE, MAP_LIMIT };
+ if (typeof module !== 'undefined' && module.exports) module.exports = api;
+ root.RegionsRoutes = api;
+})(typeof window === 'undefined' ? globalThis : window);
diff --git a/public/style.css b/public/style.css
index 6f94ceb7a..fb0dc5596 100644
--- a/public/style.css
+++ b/public/style.css
@@ -5944,6 +5944,7 @@ body.embed #app.app-fixed {
.regions-page { display: flex; flex-direction: column; height: 100%; min-height: 420px; padding: 16px; gap: 8px; }
.regions-page > :not(.regions-leaflet) { flex-shrink: 0; }
.regions-page-health { overflow-y: auto; }
+.regions-page-connections { overflow-y: auto; }
.regions-page h1, .regions-page p { margin: 0; }
.regions-note { color: var(--text-muted); max-width: 85ch; }
.regions-controls { display: flex; flex-wrap: wrap; gap: 8px 12px; align-items: center; }
@@ -5960,6 +5961,7 @@ body.embed #app.app-fixed {
.regions-slider-label input { width: 100%; min-width: 0; accent-color: var(--accent); }
.regions-compare-label { display: flex; gap: 6px; align-items: center; font-size: 0.9em; }
.regions-compare-label input { accent-color: var(--accent); }
+.regions-compare-label[hidden] { display: none; }
.regions-interval { grid-column: 1 / -1; font-size: 0.85em; color: var(--text-muted); overflow-wrap: anywhere; }
.regions-comparison { display: flex; flex-direction: column; gap: 6px; font-size: 0.85em; padding: 10px; border: 1px solid var(--border); border-radius: 8px; }
.regions-reference { color: var(--text-muted); overflow-wrap: anywhere; }
@@ -5999,6 +6001,21 @@ body.embed #app.app-fixed {
.regions-hourly-table caption { text-align: left; color: var(--text-muted); margin-bottom: 6px; }
.regions-hourly-table th, .regions-hourly-table td { text-align: left; padding: 6px 8px; border-bottom: 1px solid var(--border); }
.regions-hourly-table th { color: var(--text-muted); }
+.regions-connections { display: flex; flex-direction: column; gap: 8px; min-width: 0; padding: 12px; border: 1px solid var(--border); border-radius: 8px; }
+.regions-connections[hidden] { display: none; }
+.regions-connections h2 { margin: 4px 0 0; font-size: 1rem; }
+.regions-connections-summary, .regions-pairs-summary, .regions-steps-summary { font-size: 0.85rem; }
+.regions-connections-note, .regions-step-diagnostics, .regions-connections-map-note { color: var(--text-muted); font-size: 0.8rem; overflow-wrap: anywhere; }
+.regions-connections[data-complete="false"] .regions-connections-summary { border-left: 3px solid var(--warning); padding-left: 8px; }
+.regions-relations-scroll { max-height: 280px; max-width: 100%; overflow: auto; border: 1px solid var(--border); border-radius: 6px; }
+.regions-relations-scroll table { width: 100%; min-width: 520px; border-collapse: collapse; font-size: 0.8rem; }
+.regions-relations-scroll th, .regions-relations-scroll td { padding: 6px 8px; text-align: left; border-bottom: 1px solid var(--border); overflow-wrap: anywhere; }
+.regions-relations-scroll th { position: sticky; top: 0; color: var(--text-muted); background: var(--surface); }
+.regions-relations-pagination { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; margin-top: 6px; color: var(--text-muted); font-size: 0.8rem; }
+.regions-relations-pagination button { padding: 5px 8px; border: 1px solid var(--border); border-radius: 5px; color: var(--text-primary); background: var(--surface); cursor: pointer; }
+.regions-relations-pagination button:disabled { opacity: 0.55; cursor: default; }
+.regions-relations-scroll:focus-visible, .regions-relations-pagination button:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
+.regions-step-arrow { fill: var(--accent); }
.regions-status { color: var(--text-muted); min-height: 2.4em; }
.regions-snapshot, .regions-coverage, .regions-details { color: var(--text-muted); font-size: 0.85em; overflow-wrap: anywhere; }
.regions-details summary { cursor: pointer; }
diff --git a/test-regions-map-e2e.js b/test-regions-map-e2e.js
index 64c81b008..5a43c5f9b 100644
--- a/test-regions-map-e2e.js
+++ b/test-regions-map-e2e.js
@@ -48,6 +48,24 @@ function historySnapshot(override = {}) {
truncated: false, scopesTruncated: false, bucketsTruncated: false, ...override };
}
+function routesSnapshot(override = {}) {
+ const data = historySnapshot();
+ data.nodes = data.nodes.map(node => ({ ...node, publicKey: node.publicKey.padEnd(64, '0') }));
+ const key = short => short.padEnd(64, '0');
+ const bucket = (index, count) => ({ index, count, lastRelayed: new Date(Date.parse(data.historyStart) + index * 3600000 + 1000).toISOString() });
+ return { ...data, stepNodes: [
+ { publicKey: key('6666'), name: 'Only own flood from', role: 'repeater', lat: 57, lon: 11, positionSource: 'reported' },
+ { publicKey: key('7777'), name: 'Only own flood to', role: 'room', lat: 56, lon: 12, positionSource: 'reported' },
+ ], steps: [
+ { from: key('aa11'), to: key('bb22'), scope: '#example', buckets: [bucket(502, 2), bucket(503, 3)] },
+ { from: key('bb22'), to: key('aa11'), scope: '#example', buckets: [bucket(503, 2)] },
+ { from: key('aa11'), to: key('dd44'), scope: '#example', buckets: [bucket(503, 1)] },
+ { from: key('cc33'), to: key('bb22'), scope: '#other', buckets: [bucket(503, 4)] },
+ { from: key('6666'), to: key('7777'), scope: '#routeonly', buckets: [bucket(503, 7)] },
+ ], stepsTruncated: false, stepBucketsTruncated: false,
+ stepEvidence: { skippedObservations: 0, unresolvedHops: 0, missingEndpointSteps: 0, sourceTruncated: false, incomplete: false }, ...override };
+}
+
async function main() {
let browser;
try {
@@ -66,6 +84,7 @@ async function main() {
let historyOverride = {}, failHistory = false;
let holdHistory = false, heldHistory = null;
let historyRequests = 0;
+ let routesRequests = 0, routesOverride = {}, holdRoutes = false, heldRoutes = null;
const requests = [];
const waitForHeldHour = async () => {
const deadline = Date.now() + 5000;
@@ -88,6 +107,11 @@ async function main() {
if (failHistory) return r.fulfill({ status: 500, body: '{}' });
await r.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(historySnapshot(historyOverride)) });
});
+ await page.route('**/api/regions/routes*', async route => {
+ routesRequests++;
+ if (holdRoutes) { holdRoutes = false; heldRoutes = route; return; }
+ await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(routesSnapshot(routesOverride)) });
+ });
let failures = 0;
let coverageCapture = 0;
async function captureCoverage() {
@@ -576,6 +600,228 @@ async function main() {
assert.strictEqual(await page.locator('.regions-health').isVisible(), false);
});
+ await test('Connections distinguishes shared membership from directed scoped steps', async () => {
+ const before = routesRequests;
+ await page.evaluate(() => { location.hash = '#/regions?view=connections&window=1h&scope=%23example&at=2026-10-16T12%3A48%3A00Z&compare=1'; });
+ await page.waitForFunction(() => document.querySelector('.regions-connections')?.getAttribute('data-ready') === 'true');
+ assert.strictEqual(routesRequests, before + 1);
+ assert.strictEqual(await page.getByLabel('Region view').inputValue(), 'connections');
+ assert.strictEqual(await page.getByRole('button', { name: 'Play timeline', exact: true }).isVisible(), false);
+ assert.strictEqual(await page.getByLabel('Compare previous period').isVisible(), false);
+ assert.strictEqual(await page.locator('.regions-pairs-table tbody tr').count(), 1);
+ assert.match(await page.locator('.regions-pairs-table tbody').textContent(), /#example — #other/);
+ assert.strictEqual(await page.getByRole('heading', { name: 'Shared relay-history memberships', exact: true }).isVisible(), true);
+ assert.strictEqual(await page.getByRole('columnheader', { name: 'History identities', exact: true }).isVisible(), true);
+ const provenance = page.locator('.regions-connections-provenance');
+ assert.strictEqual(await provenance.isVisible(), true);
+ assert.match(await provenance.textContent(), /transmission-level relay history used by Timeline\/Health/);
+ assert.match(await provenance.textContent(), /Observation route\/scope variants are not independently verified here; mixed flood\/direct transmissions can include planned-path candidates/);
+ assert.match(await provenance.textContent(), /Only directed steps below use each observation’s own frame evidence/);
+ assert.match(await page.locator('.regions-pairs-summary').textContent(), /1 distinct identities with shared history membership/);
+ assert.match(await page.locator('.regions-connections-note').first().textContent(), /does not establish cross-scope traffic/);
+ assert.strictEqual(await page.locator('.regions-steps-table tbody tr').count(), 3);
+ assert.match(await page.locator('.regions-steps-summary').textContent(), /6 step appearances/);
+ assert.match(await page.locator('.regions-steps-summary').textContent(), /1 rows have endpoints without usable positions/);
+ const directions = await page.locator('.regions-steps-table tbody tr td:first-child').allTextContents();
+ assert(directions.some(value => value.includes(EVIL + ' → New relay')));
+ assert(directions.some(value => value.includes('New relay → ' + EVIL)));
+ assert.strictEqual(await page.locator('.regions-directed-step').count(), 2);
+ assert(await page.locator('.regions-directed-step').first().getAttribute('marker-end'));
+ assert.match(await page.locator('.regions-step-diagnostics').textContent(), /Snapshot-wide evidence diagnostics \(not selected-interval totals\)/);
+ assert.strictEqual(await page.locator('.regions-connections img').count(), 0);
+ assert.strictEqual(await page.evaluate(() => window.__xss), undefined);
+ const params = new URLSearchParams((await page.evaluate(() => location.hash)).split('?')[1]);
+ assert.strictEqual(params.get('at'), '2026-10-16T12:00:00.000Z');
+ assert.strictEqual(params.has('compare'), false);
+ });
+
+ await test('Connections filters frames locally and preserves route-only scope endpoints', async () => {
+ const before = routesRequests;
+ await page.evaluate(() => { window.__regionsFitCount = 0; });
+ await page.getByLabel('Region scope').selectOption('#routeonly');
+ assert.strictEqual(await page.locator('.regions-steps-table tbody tr').count(), 1);
+ assert.strictEqual(await page.locator('.regions-pairs-table tbody tr').count(), 0, 'route-only metadata created false membership');
+ assert.match(await page.locator('.regions-steps-table tbody').textContent(), /Only own flood from → Only own flood to/);
+ assert.match(await page.locator('.regions-steps-summary').textContent(), /7 step appearances/);
+ assert.strictEqual(await page.locator('.regions-directed-step').count(), 1);
+ await page.getByLabel('Timeline interval ending').focus();
+ await page.getByLabel('Timeline interval ending').press('ArrowLeft');
+ await page.waitForFunction(() => document.querySelector('.regions-steps-summary')?.textContent.includes('0 directed'));
+ assert.match(await page.locator('.regions-steps-summary').textContent(), /No qualifying directed steps were observed/);
+ await page.getByLabel('Timeline interval ending').press('End');
+ await page.getByLabel('Region period').selectOption('24h');
+ await page.getByLabel('Region scope').selectOption('#example');
+ assert.match(await page.locator('.regions-steps-summary').textContent(), /8 step appearances/);
+ assert.strictEqual(await page.evaluate(() => window.__regionsFitCount), 0);
+ assert.strictEqual(routesRequests, before);
+ await page.getByLabel('Region period').selectOption('1h');
+ });
+
+ await test('Connections qualifies partial, unknown, loading and capped evidence without absence claims', async () => {
+ for (const override of [{ coverage: { start: '2026-10-16T11:30:00Z', loading: false } },
+ { coverage: { start: null, loading: false } }, { coverage: { start: null, loading: true } },
+ { stepsTruncated: true }, { stepBucketsTruncated: true }, { stepEvidence: { sourceTruncated: true, incomplete: true } },
+ { stepEvidence: { skippedObservations: 12, unresolvedHops: 3, missingEndpointSteps: 0, sourceTruncated: false, incomplete: false } }]) {
+ routesOverride = override;
+ await page.getByRole('button', { name: 'Refresh', exact: true }).click();
+ await page.waitForFunction(() => document.querySelector('.regions-connections')?.getAttribute('data-ready') === 'true');
+ assert.strictEqual(await page.locator('.regions-connections').getAttribute('data-complete'), 'false');
+ assert.match(await page.locator('.regions-connections-summary').textContent(), /missing results do not establish absence/);
+ assert.doesNotMatch(await page.locator('.regions-connections-summary').textContent(), /lower bounds/);
+ assert.match(await page.locator('.regions-steps-summary').textContent(), /6 step appearances/);
+ }
+ routesOverride = {};
+ await page.getByRole('button', { name: 'Refresh', exact: true }).click();
+ await page.waitForFunction(() => document.querySelector('.regions-connections')?.getAttribute('data-complete') === 'true');
+ });
+
+ await test('Connections works as accessible evidence tables without Leaflet', async () => {
+ await page.evaluate(() => { window.__regionsSavedL = window.L; window.L = undefined; location.hash = '#/regions?view=connections&window=1h&scope=%23routeonly'; });
+ try {
+ await page.waitForFunction(() => document.querySelector('.regions-connections')?.getAttribute('data-ready') === 'true');
+ assert.strictEqual(await page.locator('.regions-steps-table tbody tr').count(), 1);
+ assert.match(await page.locator('.regions-status').textContent(), /evidence tables remain available/);
+ assert.strictEqual(await page.locator('.regions-leaflet.leaflet-container').count(), 0);
+ } finally { await page.evaluate(() => { window.L = window.__regionsSavedL; delete window.__regionsSavedL; }); }
+ await page.getByLabel('Region scope').selectOption('#example');
+ await page.evaluate(() => document.documentElement.setAttribute('data-theme', 'dark'));
+ assert(await page.locator('.regions-directed-step').first().evaluate(element => getComputedStyle(element).stroke !== 'none'));
+ });
+
+ await test('Connections warns when route-only scopes exceed the bounded selector and preserves an outside selection', async () => {
+ const base = routesSnapshot();
+ routesOverride = { nodes: [], steps: Array.from({ length: 129 }, (_, index) => ({ from: base.stepNodes[0].publicKey,
+ to: base.stepNodes[1].publicKey, scope: '#route' + String(index).padStart(3, '0'), buckets: [base.steps[4].buckets[0]] })) };
+ await page.evaluate(() => { location.hash = '#/regions?view=connections&window=1h&scope=%23route128'; });
+ await page.waitForFunction(() => document.querySelector('.regions-connections')?.getAttribute('data-ready') === 'true');
+ assert.strictEqual(await page.getByLabel('Region scope').inputValue(), '#route128');
+ assert.strictEqual(await page.getByLabel('Region scope').locator('option').count(), 130);
+ assert.match(await page.getByLabel('Region scope').locator('option:checked').textContent(), /Selected \(outside list\)/);
+ assert.strictEqual(await page.locator('.regions-steps-table tbody tr').count(), 1);
+ assert.strictEqual(await page.locator('.regions-pairs-table tbody tr').count(), 0);
+ assert.strictEqual(await page.locator('.regions-connections').getAttribute('data-complete'), 'false');
+ assert.match(await page.locator('.regions-connections-summary').textContent(), /missing results do not establish absence/);
+ assert.match(await page.locator('.regions-status').textContent(), /selector is limited to 128 names/);
+ routesOverride = {};
+ await page.evaluate(() => { location.hash = '#/regions?view=connections&window=1h&scope=%23example'; });
+ await page.waitForFunction(() => document.querySelector('.regions-connections')?.getAttribute('data-complete') === 'true');
+ });
+
+ await test('returning from Health refreshes Connections and Latest tiles and hidden-map dimensions', async () => {
+ const viewport = page.viewportSize();
+ await page.route('https://regions-tiles.invalid/**', route => route.fulfill({ status: 200, contentType: 'image/svg+xml',
+ body: '' }));
+ await page.evaluate(() => {
+ window.__regionsResume = { spec: window.MC_getTileSpec, invalidate: L.Map.prototype.invalidateSize,
+ fit: L.Map.prototype.fitBounds, theme: document.documentElement.getAttribute('data-theme'), invalidations: 0 };
+ window.MC_getTileSpec = theme => ({ url: 'https://regions-tiles.invalid/' + theme + '/{z}/{x}/{y}.png', maxZoom: 19 });
+ L.Map.prototype.invalidateSize = function (...args) {
+ window.__regionsResume.invalidations++; return window.__regionsResume.invalidate.apply(this, args);
+ };
+ L.Map.prototype.fitBounds = function (...args) {
+ window.__regionsResume.map = this; return window.__regionsResume.fit.apply(this, args);
+ };
+ document.documentElement.setAttribute('data-theme', 'dark');
+ });
+ try {
+ // Force a theme mutation even when the previous case already used dark.
+ await page.evaluate(() => document.documentElement.setAttribute('data-theme', 'light'));
+ await page.evaluate(() => document.documentElement.setAttribute('data-theme', 'dark'));
+ await page.waitForFunction(() => [...document.querySelectorAll('.regions-leaflet .leaflet-tile')].some(tile => tile.src.includes('regions-tiles.invalid/dark/')));
+ await page.getByRole('button', { name: 'Fit map', exact: true }).click();
+ await page.getByLabel('Region view').selectOption('health');
+ await page.evaluate(() => document.documentElement.setAttribute('data-theme', 'light'));
+ await page.setViewportSize({ width: 390, height: 844 });
+ await page.waitForTimeout(100); // Leaflet's resize frame runs while hidden.
+ await page.evaluate(() => { window.__regionsResume.invalidations = 0; });
+ await page.getByLabel('Region view').selectOption('connections');
+ const state = await page.evaluate(() => {
+ const record = window.__regionsResume, element = document.querySelector('.regions-leaflet'), size = record.map.getSize();
+ return { refreshedLightTiles: [...element.querySelectorAll('.leaflet-tile')].some(tile => tile.src.includes('regions-tiles.invalid/light/')),
+ invalidatedOnReturn: record.invalidations > 0, currentDimensions: size.x === element.clientWidth && size.y === element.clientHeight };
+ });
+ assert.deepStrictEqual(state, { refreshedLightTiles: true, invalidatedOnReturn: true, currentDimensions: true });
+ await page.getByLabel('Region view').selectOption('health');
+ await page.evaluate(() => document.documentElement.setAttribute('data-theme', 'dark'));
+ await page.setViewportSize({ width: 1440, height: 1000 });
+ await page.waitForTimeout(100);
+ await page.evaluate(() => { window.__regionsResume.invalidations = 0; });
+ await page.getByLabel('Region view').selectOption('latest');
+ await page.waitForFunction(() => document.querySelector('.regions-status')?.textContent.includes('positioned relay'));
+ const latestState = await page.evaluate(() => {
+ const record = window.__regionsResume, element = document.querySelector('.regions-leaflet'), size = record.map.getSize();
+ return { refreshedDarkTiles: [...element.querySelectorAll('.leaflet-tile')].some(tile => tile.src.includes('regions-tiles.invalid/dark/')),
+ invalidatedOnReturn: record.invalidations > 0, currentDimensions: size.x === element.clientWidth && size.y === element.clientHeight };
+ });
+ assert.deepStrictEqual(latestState, { refreshedDarkTiles: true, invalidatedOnReturn: true, currentDimensions: true });
+ } finally {
+ await page.evaluate(() => {
+ const record = window.__regionsResume;
+ window.MC_getTileSpec = record.spec; L.Map.prototype.invalidateSize = record.invalidate; L.Map.prototype.fitBounds = record.fit;
+ document.documentElement.setAttribute('data-theme', record.theme); delete window.__regionsResume;
+ });
+ await page.setViewportSize(viewport);
+ await page.unroute('https://regions-tiles.invalid/**');
+ if (await page.getByLabel('Region view').inputValue() !== 'connections') await page.getByLabel('Region view').selectOption('connections');
+ await page.waitForFunction(() => document.querySelector('.regions-connections')?.getAttribute('data-ready') === 'true');
+ }
+ });
+
+ await test('Connections paginates bounded keyed rows and caps only map rendering', async () => {
+ const base = routesSnapshot(), source = base.nodes[0].publicKey;
+ const targets = Array.from({ length: 230 }, (_, index) => ({ publicKey: (10000 + index).toString(16).padStart(64, '0'),
+ name: 'Relay ' + index, role: 'repeater', lat: 55 + (index % 20) / 100, lon: 10 + index / 1000, positionSource: 'reported' }));
+ routesOverride = { stepNodes: targets, steps: targets.map((node, index) => ({ from: source, to: node.publicKey, scope: '#example',
+ buckets: [{ index: 503, count: index + 1, lastRelayed: '2026-10-16T11:00:01Z' }] })) };
+ await page.getByRole('button', { name: 'Refresh', exact: true }).click();
+ await page.waitForFunction(() => document.querySelector('.regions-steps-summary')?.textContent.includes('230 directed'));
+ assert.strictEqual(await page.locator('.regions-steps-table tbody tr').count(), 25);
+ assert.strictEqual(await page.locator('.regions-directed-step').count(), 200);
+ const before = routesRequests;
+ await page.getByRole('button', { name: 'Next directed relay steps page', exact: true }).click();
+ assert.strictEqual(await page.locator('.regions-steps-table tbody tr').count(), 25);
+ assert.strictEqual(await page.evaluate(() => new URLSearchParams(location.hash.split('?')[1]).get('page')), '2');
+ assert.strictEqual(routesRequests, before);
+ await page.getByRole('button', { name: 'Previous directed relay steps page', exact: true }).click();
+ assert.strictEqual(await page.evaluate(() => new URLSearchParams(location.hash.split('?')[1]).has('page')), false);
+ routesOverride = {};
+ await page.getByRole('button', { name: 'Refresh', exact: true }).click();
+ await page.waitForFunction(() => document.querySelector('.regions-steps-summary')?.textContent.includes('3 directed'));
+ assert.strictEqual(await page.locator('.regions-directed-step').count(), 2);
+ });
+
+ if (process.env.REGIONS_SCREENSHOT_DIR) {
+ await page.setViewportSize({ width: 1440, height: 1000 });
+ await page.screenshot({ path: path.join(process.env.REGIONS_SCREENSHOT_DIR, 'regions-connections-desktop.png'), fullPage: true });
+ await page.setViewportSize({ width: 390, height: 844 });
+ await page.screenshot({ path: path.join(process.env.REGIONS_SCREENSHOT_DIR, 'regions-connections-mobile.png'), fullPage: true });
+ await test('Connections tables and controls fit narrow mobile layouts', async () => {
+ assert(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth + 1));
+ await page.locator('.regions-relations-scroll').first().focus();
+ assert(await page.locator('.regions-relations-scroll').first().evaluate(element => document.activeElement === element));
+ });
+ await page.locator('.regions-leaflet').scrollIntoViewIfNeeded();
+ await page.screenshot({ path: path.join(process.env.REGIONS_SCREENSHOT_DIR, 'regions-connections-mobile-map.png'), fullPage: true });
+ await page.setViewportSize({ width: 1440, height: 1000 });
+ }
+
+ await test('late Connections evidence cannot replace a newer historical view', async () => {
+ holdRoutes = true;
+ await page.getByRole('button', { name: 'Refresh', exact: true }).click();
+ const deadline = Date.now() + 5000;
+ while (!heldRoutes) { if (Date.now() > deadline) throw new Error('Timed out waiting for delayed routes'); await page.waitForTimeout(10); }
+ assert.strictEqual(await page.locator('.regions-connections').isVisible(), false);
+ await page.getByLabel('Region view').selectOption('health');
+ await page.waitForFunction(() => document.querySelector('.regions-health')?.getAttribute('data-ready') === 'true');
+ const request = heldRoutes.request(), finished = page.waitForResponse(response => response.request() === request);
+ await heldRoutes.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(routesSnapshot({ steps: [] })) }); heldRoutes = null;
+ await finished; await page.waitForTimeout(100);
+ assert.strictEqual(await page.getByLabel('Region view').inputValue(), 'health');
+ assert.strictEqual(await page.locator('.regions-connections').isVisible(), false);
+ assert.strictEqual(await page.locator('.regions-leaflet').isVisible(), false);
+ await page.getByLabel('Region view').selectOption('timeline');
+ });
+
await test('destroy() tears down the map and the theme observer', async () => {
// Navigation during Leaflet's 250ms zoom transition used to leave a
// callback touching a removed map pane. Exercise the real zoom control.
diff --git a/test-regions-map.js b/test-regions-map.js
index acb5bb555..dd0d6aa4b 100644
--- a/test-regions-map.js
+++ b/test-regions-map.js
@@ -40,6 +40,13 @@ assert.equal(regions.validScope('#bad node('p' + i, { scopes: [scope('#r' + i)] }));
assert.equal(regions.scopeCatalog(manyScopes).truncated, true);
assert.equal(regions.scopeCatalog(manyScopes).scopes.length, 128);
+const routeOnlyCatalog = regions.mergeScopeCatalog({ scopes: [], truncated: false },
+ Array.from({ length: 129 }, (_, i) => '#route' + String(i).padStart(3, '0')));
+assert.equal(routeOnlyCatalog.truncated, true, 'route-only scopes participate in the selector cap');
+assert.equal(routeOnlyCatalog.scopes.length, 128);
+assert.equal(routeOnlyCatalog.scopes.includes('#route128'), false);
+assert.deepEqual(regions.mergeScopeCatalog({ scopes: ['#history'], truncated: false }, ['#route', '#route', '']),
+ { scopes: ['#history', '#route'], truncated: false });
assert.equal(regions.collectScopeNodes(Array.from({ length: 5001 }, (_, i) => node('p' + i))).length, 5000);
assert.deepEqual(regions.convexHull([[10, 55], [11, 55], [10, 56], [10.2, 55.2]]), [[10, 55], [11, 55], [10, 56]]);
assert.deepEqual(regions.convexHull([[10, 55], [11, 55]]), []);
@@ -270,4 +277,90 @@ for (const ending of ['2026-10-26T12:00:00Z', '2026-11-02T12:00:00Z']) {
assert.equal(dstHealth.bins[167].end, new Date(end).toISOString());
}
-console.log('regions map, timeline and activity unit tests passed');
+const routes = require('./public/regions-routes.js');
+const fullKey = letter => letter.repeat(64);
+const routeNodes = [historical(fullKey('a'), [], { scopes: [{ name: '#a|#b', buckets: [bucket(503)] }, { name: '#c', buckets: [bucket(503)] }] }),
+ historical(fullKey('b'), [], { scopes: [{ name: '#a', buckets: [bucket(503)] }, { name: '#b|#c', buckets: [bucket(503)] }] }),
+ historical(fullKey('c'), [], { positionSource: 'none', lat: null, lon: null, scopes: [{ name: '#a', buckets: [bucket(503)] }, { name: '#c', buckets: [bucket(503)] }] })];
+const routeData = { ...historyData(), nodes: routeNodes, steps: [
+ { from: fullKey('a'), to: fullKey('b'), scope: '#a', buckets: [bucket(503, 3)] },
+ { from: fullKey('b'), to: fullKey('a'), scope: '#a', buckets: [bucket(503, 2)] },
+ { from: fullKey('a'), to: fullKey('c'), scope: '#c', buckets: [bucket(503)] },
+], stepEvidence: { skippedObservations: 0, unresolvedHops: 0, sourceTruncated: false, incomplete: false } };
+const routeHistory = history.indexHistory(routeData, routeNodes);
+let routeIndex = routes.indexRoutes(routeData, routeHistory, regions.validScope);
+let routeFrame = history.selectFrame(routeHistory, { window: '1h' });
+let connections = routes.selectRoutes(routeIndex, routeHistory, routeFrame, {});
+assert.equal(connections.steps.length, 3);
+assert.equal(connections.stepAppearances, 6, 'step totals count edge appearances, not global distinct packets');
+assert.equal(connections.unmappedSteps, 1);
+assert.equal(connections.pairs.length, 3, 'scope names containing delimiters must not collide');
+assert.equal(connections.pairs.find(pair => pair.from === '#a|#b' && pair.to === '#c').count, 1);
+assert.equal(connections.pairs.find(pair => pair.from === '#a' && pair.to === '#b|#c').count, 1);
+assert.equal(connections.complete, true);
+connections = routes.selectRoutes(routeIndex, routeHistory, routeFrame, { scope: '#a' });
+assert.equal(connections.steps.length, 2);
+assert.equal(connections.pairs.length, 2);
+assert(connections.steps.some(step => step.from === fullKey('a') && step.to === fullKey('b')));
+assert(connections.steps.some(step => step.from === fullKey('b') && step.to === fullKey('a')));
+const limitedData = { ...routeData, stepBucketsTruncated: true };
+connections = routes.selectRoutes(routes.indexRoutes(limitedData, routeHistory, regions.validScope), routeHistory, routeFrame, {});
+assert.equal(connections.complete, false);
+assert.equal(connections.stepAppearances, 6, 'limited evidence remains an explicit lower bound');
+for (const key of ['skippedObservations', 'unresolvedHops', 'missingEndpointSteps']) {
+ const contradictory = { ...routeData, stepEvidence: { ...routeData.stepEvidence, [key]: 1, incomplete: false } };
+ const selected = routes.selectRoutes(routes.indexRoutes(contradictory, routeHistory, regions.validScope), routeHistory, routeFrame, {});
+ assert.equal(selected.complete, false, 'omitted evidence cannot be complete even when the server flag contradicts diagnostics');
+ assert.equal(selected.stepAppearances, 6);
+}
+const badRoutes = { ...routeData, steps: [{ ...routeData.steps[0], to: 'bad' }, { ...routeData.steps[0], scope: '' },
+ { ...routeData.steps[0], buckets: [bucket(504)] }] };
+assert.equal(routes.indexRoutes(badRoutes, routeHistory, regions.validScope).limited, true);
+const cliqueNodes = Array.from({ length: 5000 }, (_, i) => historical(i.toString(16).padStart(64, '0'), [], {
+ scopes: Array.from({ length: 32 }, (_, j) => ({ name: '#scope' + j, buckets: [bucket(503)] })),
+}));
+const cliqueHistory = history.indexHistory(historyData(), cliqueNodes);
+const cliqueIndex = routes.indexRoutes({ steps: [] }, cliqueHistory, regions.validScope);
+const cliqueFrame = history.selectFrame(cliqueHistory, { window: '1h' });
+const clique = routes.selectRoutes(cliqueIndex, cliqueHistory, cliqueFrame, {});
+assert.equal(clique.pairs.length, 496);
+assert(clique.pairs.every(pair => pair.count === 5000), 'retained pairs have exact distinct-relay weights');
+assert(clique.pairs.every(pair => pair.examples.length <= 3));
+assert(clique.pairs.length <= 1000);
+assert.equal(routes.selectRoutes(cliqueIndex, cliqueHistory, cliqueFrame, {}).pairs, clique.pairs, 'same interval reuses bounded relation aggregation');
+const routeOnlyData = { ...routeData, stepNodes: [
+ { publicKey: fullKey('d'), name: 'Route only from', role: 'repeater', lat: 55, lon: 10, positionSource: 'reported' },
+ { publicKey: fullKey('e'), name: 'Route only to', role: 'room', lat: 56, lon: 11, positionSource: 'reported' },
+], steps: [{ from: fullKey('d'), to: fullKey('e'), scope: '#routeonly', buckets: [bucket(503, 7)] }] };
+const routeOnly = routes.selectRoutes(routes.indexRoutes(routeOnlyData, routeHistory, regions.validScope, regions.validPosition), routeHistory, routeFrame, { scope: '#routeonly' });
+assert.equal(routeOnly.steps.length, 1, 'own flood endpoint metadata is independent of canonical-route relay history');
+assert.equal(routeOnly.stepAppearances, 7);
+assert.equal(routeOnly.pairs.length, 0, 'route-only endpoints must not create scope membership');
+assert.equal(routeOnly.mapSteps.length, 1);
+assert.equal(regions.parseSelection('#/regions?view=connections&window=1h&scope=%23routeonly&compare=1&page=2').compare, false);
+assert.equal(regions.scopeHash('#routeonly', '1h', { view: 'connections', at: historyEnd, page: 2, pairsPage: 3, compare: true }),
+ '#/regions?window=1h&scope=%23routeonly&view=connections&at=2026-10-16T12%3A00%3A00.000Z&page=2&pairsPage=3');
+assert.equal(history.scopeCount(cliqueHistory.nodes[0].scopes[0], 503, 504).count, 1, 'connections reuse the existing sparse range reader');
+const populatedSteps = Array.from({ length: 5000 }, (_, index) => ({ from: index.toString(16).padStart(64, '0'),
+ to: ((index + 1) % 5000).toString(16).padStart(64, '0'), scope: '#scope0', buckets: Array.from({ length: 20 }, (_, n) => bucket(484 + n)) }));
+const fullRoutes = routes.indexRoutes({ steps: populatedSteps, stepEvidence: routeData.stepEvidence }, cliqueHistory, regions.validScope);
+const fullConnections = routes.selectRoutes(fullRoutes, cliqueHistory, cliqueFrame, { scope: '#scope0' });
+assert.equal(fullConnections.steps.length, 5000);
+assert.equal(fullConnections.mapSteps.length, 200, 'the map has a separate bounded rendering budget');
+assert.equal(fullRoutes.steps.reduce((sum, step) => sum + step.indices.length, 0), 100000);
+const cappedSteps = routes.indexRoutes({ steps: [...populatedSteps, populatedSteps[0]], stepEvidence: routeData.stepEvidence }, cliqueHistory, regions.validScope);
+assert.equal(cappedSteps.limited, true, 'row caps cannot silently imply complete evidence');
+const diverseNodes = Array.from({ length: 80 }, (_, i) => historical((10000 + i).toString(16).padStart(64, '0'), [], {
+ scopes: Array.from({ length: 32 }, (_, j) => ({ name: '#group' + i + 's' + j, buckets: [bucket(503)] })),
+}));
+const diverseHistory = history.indexHistory(historyData(), diverseNodes);
+const diverseFrame = history.selectFrame(diverseHistory, { window: '1h' });
+const diverseIndex = routes.indexRoutes({ steps: [] }, diverseHistory, regions.validScope);
+const cappedPairs = routes.selectRoutes(diverseIndex, diverseHistory, diverseFrame, {});
+assert.equal(cappedPairs.pairs.length, 1000);
+assert.equal(cappedPairs.pairsTruncated, true);
+assert(cappedPairs.pairs.every(pair => pair.count === 1), 'retained pair counts stay exact after discovery reaches its cap');
+assert.equal(routes.selectRoutes(diverseIndex, diverseHistory, diverseFrame, { scope: '#group79s0' }).pairs.length, 31,
+ 'scope filtering finds exact pairs beyond the unfiltered retained subset');
+
+console.log('regions map, timeline, activity and connection unit tests passed');