Files
Kpa-clawbot a371d35bfd feat(#847): dedupe Top Longest Hops by pair + add obs count and SNR cues (#848)
## Problem

The "Top 20 Longest Hops" RF analytics card shows the same repeater pair
filling most slots because the query sorts raw hop records by distance
with no pair deduplication. A single long link observed 12+ times
dominates the leaderboard.

## Fix

Dedupe by unordered `(pk1, pk2)` pair. Per pair, keep the max-distance
record and compute reliability metrics:

| Column | Description |
|--------|-------------|
| **Obs** | Total observations of this link |
| **Best SNR** | Maximum SNR seen (dB) |
| **Median SNR** | Median SNR across all observations (dB) |

Tooltip on each row shows the timestamp of the best observation.

### Before
| # | From | To | Distance | Type | SNR | Packet |
|---|------|----|----------|------|-----|--------|
| 1 | NodeX | NodeY | 200 mi | R↔R | 5 dB | abc… |
| 2 | NodeX | NodeY | 199 mi | R↔R | 6 dB | def… |
| 3 | NodeX | NodeY | 198 mi | R↔R | 4 dB | ghi… |

### After
| # | From | To | Distance | Type | Obs | Best SNR | Median SNR | Packet
|

|---|------|----|----------|------|-----|----------|------------|--------|
| 1 | NodeX | NodeY | 200 mi | R↔R | 12 | 8.0 dB | 5.2 dB | abc… |
| 2 | NodeA | NodeB | 150 mi | C↔R | 3 | 6.5 dB | 6.5 dB | jkl… |

## Changes

- **`cmd/server/store.go`**: Group `filteredHops` by unordered pair key,
accumulate obs count / best SNR / median SNR per group, sort by max
distance, take top 20
- **`cmd/server/types.go`**: Update `DistanceHop` struct — replace `SNR`
with `BestSnr`, `MedianSnr`, add `ObsCount`
- **`public/analytics.js`**: Replace single SNR column with Obs, Best
SNR, Median SNR; add row tooltip with best observation timestamp
- **`cmd/server/store_tophops_test.go`**: 3 unit tests — basic dedupe,
reverse-pair merge, nil SNR edge case

## Test Coverage

- `TestDedupeTopHopsByPair`: 5 records on pair (A,B) + 1 on (C,D) → 2
results, correct obsCount/dist/bestSnr/medianSnr
- `TestDedupeTopHopsReversePairMerges`: (B,A) and (A,B) merge into one
entry
- `TestDedupeTopHopsNilSNR`: all-nil SNR records → bestSnr and medianSnr
both nil
- Existing `TestAnalyticsRFEndpoint` and `TestAnalyticsRFWithRegion`
still pass

Closes #847

---------

Co-authored-by: you <you@example.com>
2026-04-21 09:09:39 -07:00

1590 lines
33 KiB
JSON

{
"analytics_distance": {
"type": "object",
"keys": {
"summary": {
"type": "object",
"keys": {
"totalHops": {
"type": "number"
},
"totalPaths": {
"type": "number"
},
"avgDist": {
"type": "number"
},
"maxDist": {
"type": "number"
}
}
},
"topHops": {
"type": "array",
"elementShape": {
"type": "object",
"keys": {
"fromName": {
"type": "string"
},
"fromPk": {
"type": "string"
},
"toName": {
"type": "string"
},
"toPk": {
"type": "string"
},
"dist": {
"type": "number"
},
"type": {
"type": "string"
},
"hash": {
"type": "string"
},
"timestamp": {
"type": "string"
},
"bestSnr": {
"type": "number"
},
"medianSnr": {
"type": "number"
},
"obsCount": {
"type": "number"
}
}
}
},
"topPaths": {
"type": "array",
"elementShape": {
"type": "object",
"keys": {
"hash": {
"type": "string"
},
"totalDist": {
"type": "number"
},
"hopCount": {
"type": "number"
},
"timestamp": {
"type": "string"
},
"hops": {
"type": "array",
"elementShape": {
"type": "object"
}
}
}
}
},
"catStats": {
"type": "object",
"dynamicKeys": true,
"valueShape": {
"type": "object",
"keys": {
"count": {
"type": "number"
},
"avg": {
"type": "number"
},
"median": {
"type": "number"
},
"min": {
"type": "number"
},
"max": {
"type": "number"
}
}
}
},
"distHistogram": {
"type": "object",
"keys": {
"bins": {
"type": "array",
"elementShape": {
"type": "object",
"keys": {
"x": {
"type": "number"
},
"w": {
"type": "number"
},
"count": {
"type": "number"
}
}
}
},
"min": {
"type": "number"
},
"max": {
"type": "number"
}
}
},
"distOverTime": {
"type": "array",
"elementShape": {
"type": "object",
"keys": {
"hour": {
"type": "string"
},
"avg": {
"type": "number"
},
"count": {
"type": "number"
}
}
}
}
}
},
"analytics_hash_sizes": {
"type": "object",
"keys": {
"total": {
"type": "number"
},
"distribution": {
"type": "object",
"dynamicKeys": true,
"valueShape": {
"type": "number"
}
},
"hourly": {
"type": "array",
"elementShape": {
"type": "object",
"keys": {
"hour": {
"type": "string"
},
"1": {
"type": "number"
},
"2": {
"type": "number"
},
"3": {
"type": "number"
}
}
}
},
"topHops": {
"type": "array",
"elementShape": {
"type": "object",
"keys": {
"hex": {
"type": "string"
},
"size": {
"type": "number"
},
"count": {
"type": "number"
},
"name": {
"type": "nullable"
},
"pubkey": {
"type": "nullable"
}
}
}
},
"multiByteNodes": {
"type": "array",
"elementShape": {
"type": "object",
"keys": {
"name": {
"type": "string"
},
"hashSize": {
"type": "number"
},
"packets": {
"type": "number"
},
"lastSeen": {
"type": "string"
},
"pubkey": {
"type": "string"
}
}
}
}
}
},
"analytics_rf": {
"type": "object",
"keys": {
"totalPackets": {
"type": "number"
},
"totalAllPackets": {
"type": "number"
},
"totalTransmissions": {
"type": "number"
},
"snr": {
"type": "object",
"keys": {
"min": {
"type": "number"
},
"max": {
"type": "number"
},
"avg": {
"type": "number"
},
"median": {
"type": "number"
},
"stddev": {
"type": "number"
}
}
},
"rssi": {
"type": "object",
"keys": {
"min": {
"type": "number"
},
"max": {
"type": "number"
},
"avg": {
"type": "number"
},
"median": {
"type": "number"
},
"stddev": {
"type": "number"
}
}
},
"snrValues": {
"type": "object",
"keys": {
"bins": {
"type": "array",
"elementShape": {
"type": "object",
"keys": {
"x": {
"type": "number"
},
"w": {
"type": "number"
},
"count": {
"type": "number"
}
}
}
},
"min": {
"type": "number"
},
"max": {
"type": "number"
}
}
},
"rssiValues": {
"type": "object",
"keys": {
"bins": {
"type": "array",
"elementShape": {
"type": "object",
"keys": {
"x": {
"type": "number"
},
"w": {
"type": "number"
},
"count": {
"type": "number"
}
}
}
},
"min": {
"type": "number"
},
"max": {
"type": "number"
}
}
},
"packetSizes": {
"type": "object",
"keys": {
"bins": {
"type": "array",
"elementShape": {
"type": "object",
"keys": {
"x": {
"type": "number"
},
"w": {
"type": "number"
},
"count": {
"type": "number"
}
}
}
},
"min": {
"type": "number"
},
"max": {
"type": "number"
}
}
},
"minPacketSize": {
"type": "number"
},
"maxPacketSize": {
"type": "number"
},
"avgPacketSize": {
"type": "number"
},
"packetsPerHour": {
"type": "array",
"elementShape": {
"type": "object",
"keys": {
"hour": {
"type": "string"
},
"count": {
"type": "number"
}
}
}
},
"payloadTypes": {
"type": "array",
"elementShape": {
"type": "object",
"keys": {
"type": {
"type": "nullable_number"
},
"name": {
"type": "string"
},
"count": {
"type": "number"
}
}
}
},
"snrByType": {
"type": "array",
"elementShape": {
"type": "object",
"keys": {
"name": {
"type": "string"
},
"count": {
"type": "number"
},
"avg": {
"type": "number"
},
"min": {
"type": "number"
},
"max": {
"type": "number"
}
}
}
},
"signalOverTime": {
"type": "array",
"elementShape": {
"type": "object",
"keys": {
"hour": {
"type": "string"
},
"count": {
"type": "number"
},
"avgSnr": {
"type": "number"
}
}
}
},
"scatterData": {
"type": "array",
"elementShape": {
"type": "object",
"keys": {
"snr": {
"type": "number"
},
"rssi": {
"type": "number"
}
}
}
},
"timeSpanHours": {
"type": "number"
}
}
},
"analytics_subpaths": {
"type": "object",
"keys": {
"subpaths": {
"type": "array",
"elementShape": {
"type": "object",
"keys": {
"path": {
"type": "string"
},
"rawHops": {
"type": "array",
"elementShape": {
"type": "string"
}
},
"count": {
"type": "number"
},
"hops": {
"type": "number"
},
"pct": {
"type": "number"
}
}
}
},
"totalPaths": {
"type": "number"
}
}
},
"analytics_topology": {
"type": "object",
"keys": {
"uniqueNodes": {
"type": "number"
},
"avgHops": {
"type": "number"
},
"medianHops": {
"type": "number"
},
"maxHops": {
"type": "number"
},
"hopDistribution": {
"type": "array",
"elementShape": {
"type": "object",
"keys": {
"hops": {
"type": "number"
},
"count": {
"type": "number"
}
}
}
},
"topRepeaters": {
"type": "array",
"elementShape": {
"type": "object",
"keys": {
"hop": {
"type": "string"
},
"count": {
"type": "number"
},
"name": {
"type": "nullable"
},
"pubkey": {
"type": "nullable"
}
}
}
},
"topPairs": {
"type": "array",
"elementShape": {
"type": "object",
"keys": {
"hopA": {
"type": "string"
},
"hopB": {
"type": "string"
},
"count": {
"type": "number"
},
"nameA": {
"type": "nullable"
},
"nameB": {
"type": "nullable"
},
"pubkeyA": {
"type": "nullable"
},
"pubkeyB": {
"type": "nullable"
}
}
}
},
"hopsVsSnr": {
"type": "array",
"elementShape": {
"type": "object",
"keys": {
"hops": {
"type": "number"
},
"count": {
"type": "number"
},
"avgSnr": {
"type": "number"
}
}
}
},
"observers": {
"type": "array",
"elementShape": {
"type": "object",
"keys": {
"id": {
"type": "string"
},
"name": {
"type": "string"
}
}
}
},
"perObserverReach": {
"type": "object",
"dynamicKeys": true,
"valueShape": {
"type": "object",
"keys": {
"observer_name": {
"type": "string"
},
"rings": {
"type": "array",
"elementShape": {
"type": "object"
}
}
}
}
},
"multiObsNodes": {
"type": "array",
"elementShape": {
"type": "object",
"keys": {
"hop": {
"type": "string"
},
"name": {
"type": "nullable"
},
"pubkey": {
"type": "nullable"
},
"observers": {
"type": "array",
"elementShape": {
"type": "object"
}
}
}
}
},
"bestPathList": {
"type": "array",
"elementShape": {
"type": "object",
"keys": {
"hop": {
"type": "string"
},
"name": {
"type": "nullable"
},
"pubkey": {
"type": "nullable"
},
"minDist": {
"type": "number"
},
"observer_id": {
"type": "string"
},
"observer_name": {
"type": "string"
}
}
}
}
}
},
"bulk_health": {
"type": "array",
"elementShape": {
"type": "object",
"keys": {
"public_key": {
"type": "string"
},
"name": {
"type": "string"
},
"role": {
"type": "string"
},
"lat": {
"type": "number"
},
"lon": {
"type": "number"
},
"stats": {
"type": "object",
"keys": {
"totalTransmissions": {
"type": "number"
},
"totalObservations": {
"type": "number"
},
"totalPackets": {
"type": "number"
},
"packetsToday": {
"type": "number"
},
"avgSnr": {
"type": "nullable"
},
"lastHeard": {
"type": "nullable"
}
}
},
"observers": {
"type": "array",
"elementShape": {
"type": "object",
"keys": {
"observer_id": {
"type": "string"
},
"observer_name": {
"type": "string"
},
"avgSnr": {
"type": "nullable"
},
"avgRssi": {
"type": "nullable"
},
"packetCount": {
"type": "number"
}
}
}
}
}
}
},
"channel_messages": {
"type": "object",
"keys": {
"messages": {
"type": "array",
"elementShape": {
"type": "object",
"keys": {
"sender": {
"type": "string"
},
"text": {
"type": "string"
},
"timestamp": {
"type": "string"
},
"sender_timestamp": {
"type": "number"
},
"packetId": {
"type": "number"
},
"packetHash": {
"type": "string"
},
"repeats": {
"type": "number"
},
"observers": {
"type": "array",
"elementShape": {
"type": "string"
}
},
"hops": {
"type": "number"
},
"snr": {
"type": "nullable"
}
}
}
},
"total": {
"type": "number"
}
}
},
"channels": {
"type": "object",
"keys": {
"channels": {
"type": "array",
"elementShape": {
"type": "object",
"keys": {
"hash": {
"type": "string"
},
"name": {
"type": "string"
},
"lastMessage": {
"type": "string"
},
"lastSender": {
"type": "string"
},
"messageCount": {
"type": "number"
},
"lastActivity": {
"type": "string"
}
}
}
}
}
},
"health": {
"type": "object",
"keys": {
"status": {
"type": "string"
},
"uptime": {
"type": "number"
},
"uptimeHuman": {
"type": "string"
},
"memory": {
"type": "object",
"keys": {
"rss": {
"type": "number"
},
"heapUsed": {
"type": "number"
},
"heapTotal": {
"type": "number"
},
"external": {
"type": "number"
}
}
},
"eventLoop": {
"type": "object",
"keys": {
"currentLagMs": {
"type": "number"
},
"maxLagMs": {
"type": "number"
},
"p50Ms": {
"type": "number"
},
"p95Ms": {
"type": "number"
},
"p99Ms": {
"type": "number"
}
}
},
"cache": {
"type": "object",
"keys": {
"entries": {
"type": "number"
},
"hits": {
"type": "number"
},
"misses": {
"type": "number"
},
"staleHits": {
"type": "number"
},
"recomputes": {
"type": "number"
},
"hitRate": {
"type": "number"
}
}
},
"websocket": {
"type": "object",
"keys": {
"clients": {
"type": "number"
}
}
},
"packetStore": {
"type": "object",
"keys": {
"packets": {
"type": "number"
},
"estimatedMB": {
"type": "number"
},
"trackedMB": {
"type": "number"
}
}
},
"perf": {
"type": "object",
"keys": {
"totalRequests": {
"type": "number"
},
"avgMs": {
"type": "number"
},
"slowQueries": {
"type": "number"
},
"recentSlow": {
"type": "array",
"elementShape": {
"type": "object",
"keys": {
"path": {
"type": "string"
},
"ms": {
"type": "number"
},
"time": {
"type": "string"
},
"status": {
"type": "number"
}
}
}
}
}
}
}
},
"node_detail": {
"type": "object",
"keys": {
"node": {
"type": "object",
"keys": {
"public_key": {
"type": "string"
},
"name": {
"type": "string"
},
"role": {
"type": "string"
},
"lat": {
"type": "number"
},
"lon": {
"type": "number"
},
"last_seen": {
"type": "string"
},
"first_seen": {
"type": "string"
},
"advert_count": {
"type": "number"
},
"hash_size": {
"type": "number"
},
"hash_size_inconsistent": {
"type": "boolean"
},
"hash_sizes_seen": {
"type": "array",
"elementShape": {
"type": "number"
}
},
"battery_mv": {
"type": "nullable_number"
},
"temperature_c": {
"type": "nullable_number"
}
}
},
"recentAdverts": {
"type": "array",
"elementShape": {
"type": "object",
"keys": {
"id": {
"type": "number"
},
"raw_hex": {
"type": "string"
},
"hash": {
"type": "string"
},
"first_seen": {
"type": "string"
},
"timestamp": {
"type": "string"
},
"route_type": {
"type": "number"
},
"payload_type": {
"type": "number"
},
"decoded_json": {
"type": "string"
},
"observations": {
"type": "array",
"elementShape": {
"type": "object"
}
},
"observation_count": {
"type": "number"
},
"observer_id": {
"type": "string"
},
"observer_name": {
"type": "string"
},
"snr": {
"type": "nullable"
},
"rssi": {
"type": "nullable"
},
"path_json": {
"type": "string"
}
}
}
}
}
},
"nodes": {
"type": "object",
"keys": {
"nodes": {
"type": "array",
"elementShape": {
"type": "object",
"keys": {
"public_key": {
"type": "string"
},
"name": {
"type": "string"
},
"role": {
"type": "string"
},
"lat": {
"type": "number"
},
"lon": {
"type": "number"
},
"last_seen": {
"type": "string"
},
"first_seen": {
"type": "string"
},
"advert_count": {
"type": "number"
},
"hash_size": {
"type": "number"
},
"hash_size_inconsistent": {
"type": "boolean"
},
"last_heard": {
"type": "string"
},
"battery_mv": {
"type": "nullable_number"
},
"temperature_c": {
"type": "nullable_number"
}
}
}
},
"total": {
"type": "number"
},
"counts": {
"type": "object",
"keys": {
"repeaters": {
"type": "number"
},
"rooms": {
"type": "number"
},
"companions": {
"type": "number"
},
"sensors": {
"type": "number"
}
}
}
}
},
"observers": {
"type": "object",
"keys": {
"observers": {
"type": "array",
"elementShape": {
"type": "object",
"keys": {
"id": {
"type": "string"
},
"name": {
"type": "string"
},
"iata": {
"type": "string"
},
"last_seen": {
"type": "string"
},
"first_seen": {
"type": "string"
},
"packet_count": {
"type": "number"
},
"model": {
"type": "nullable"
},
"firmware": {
"type": "nullable"
},
"client_version": {
"type": "nullable"
},
"radio": {
"type": "nullable"
},
"battery_mv": {
"type": "nullable"
},
"uptime_secs": {
"type": "nullable"
},
"noise_floor": {
"type": "nullable"
},
"packetsLastHour": {
"type": "number"
},
"lat": {
"type": "nullable"
},
"lon": {
"type": "nullable"
},
"nodeRole": {
"type": "nullable"
}
}
}
},
"server_time": {
"type": "string"
}
}
},
"packets": {
"type": "object",
"keys": {
"packets": {
"type": "array",
"elementShape": {
"type": "object",
"keys": {
"id": {
"type": "number"
},
"raw_hex": {
"type": "string"
},
"hash": {
"type": "string"
},
"first_seen": {
"type": "string"
},
"timestamp": {
"type": "string"
},
"route_type": {
"type": "number"
},
"payload_type": {
"type": "number"
},
"decoded_json": {
"type": "string"
},
"observation_count": {
"type": "number"
},
"observer_id": {
"type": "string"
},
"observer_name": {
"type": "string"
},
"snr": {
"type": "nullable"
},
"rssi": {
"type": "nullable"
},
"path_json": {
"type": "string"
}
}
}
},
"total": {
"type": "number"
}
}
},
"packets_grouped": {
"type": "object",
"keys": {
"packets": {
"type": "array",
"elementShape": {
"type": "object",
"keys": {
"hash": {
"type": "string"
},
"first_seen": {
"type": "string"
},
"count": {
"type": "number"
},
"observer_count": {
"type": "number"
},
"latest": {
"type": "string"
},
"observer_id": {
"type": "string"
},
"observer_name": {
"type": "string"
},
"path_json": {
"type": "string"
},
"payload_type": {
"type": "number"
},
"route_type": {
"type": "number"
},
"raw_hex": {
"type": "string"
},
"decoded_json": {
"type": "string"
},
"observation_count": {
"type": "number"
},
"snr": {
"type": "nullable"
},
"rssi": {
"type": "nullable"
}
}
}
},
"total": {
"type": "number"
}
}
},
"perf": {
"type": "object",
"keys": {
"uptime": {
"type": "number"
},
"totalRequests": {
"type": "number"
},
"avgMs": {
"type": "number"
},
"endpoints": {
"type": "object",
"dynamicKeys": true,
"valueShape": {
"type": "object",
"keys": {
"count": {
"type": "number"
},
"avgMs": {
"type": "number"
},
"p50Ms": {
"type": "number"
},
"p95Ms": {
"type": "number"
},
"maxMs": {
"type": "number"
}
}
}
},
"slowQueries": {
"type": "array",
"elementShape": {
"type": "object",
"keys": {
"path": {
"type": "string"
},
"ms": {
"type": "number"
},
"time": {
"type": "string"
},
"status": {
"type": "number"
}
}
}
},
"cache": {
"type": "object",
"keys": {
"size": {
"type": "number"
},
"hits": {
"type": "number"
},
"misses": {
"type": "number"
},
"staleHits": {
"type": "number"
},
"recomputes": {
"type": "number"
},
"hitRate": {
"type": "number"
}
}
},
"packetStore": {
"type": "object",
"keys": {
"totalLoaded": {
"type": "number"
},
"totalObservations": {
"type": "number"
},
"evicted": {
"type": "number"
},
"inserts": {
"type": "number"
},
"queries": {
"type": "number"
},
"inMemory": {
"type": "number"
},
"sqliteOnly": {
"type": "boolean"
},
"maxPackets": {
"type": "number"
},
"estimatedMB": {
"type": "number"
},
"maxMB": {
"type": "number"
},
"indexes": {
"type": "object",
"keys": {
"byHash": {
"type": "number"
},
"byObserver": {
"type": "number"
},
"byNode": {
"type": "number"
},
"advertByObserver": {
"type": "number"
}
}
}
}
},
"sqlite": {
"type": "object",
"keys": {
"dbSizeMB": {
"type": "number"
},
"walSizeMB": {
"type": "number"
},
"freelistMB": {
"type": "number"
},
"walPages": {
"type": "object",
"keys": {
"total": {
"type": "number"
},
"checkpointed": {
"type": "number"
},
"busy": {
"type": "number"
}
}
},
"rows": {
"type": "object",
"keys": {
"transmissions": {
"type": "number"
},
"observations": {
"type": "number"
},
"nodes": {
"type": "number"
},
"observers": {
"type": "number"
}
}
}
}
},
"goRuntime": {
"type": "object",
"keys": {
"goroutines": {
"type": "number"
},
"numGC": {
"type": "number"
},
"pauseTotalMs": {
"type": "number"
},
"lastPauseMs": {
"type": "number"
},
"heapAllocMB": {
"type": "number"
},
"heapSysMB": {
"type": "number"
},
"heapInuseMB": {
"type": "number"
},
"heapIdleMB": {
"type": "number"
},
"numCPU": {
"type": "number"
}
}
}
}
},
"stats": {
"type": "object",
"keys": {
"totalPackets": {
"type": "number"
},
"totalTransmissions": {
"type": "number"
},
"totalObservations": {
"type": "number"
},
"totalNodes": {
"type": "number"
},
"totalNodesAllTime": {
"type": "number"
},
"totalObservers": {
"type": "number"
},
"packetsLastHour": {
"type": "number"
},
"packetsLast24h": {
"type": "number"
},
"counts": {
"type": "object",
"keys": {
"repeaters": {
"type": "number"
},
"rooms": {
"type": "number"
},
"companions": {
"type": "number"
},
"sensors": {
"type": "number"
}
}
}
}
}
}