mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-09-27 01:07:53 +00:00
fix: View Path positions observers from their own GPS, not just the IATA table
GetPacketPath only ever tried the hardcoded iataCoords table to place an observer. Any station whose configured IATA code isn't a real airport (a custom/regional code, or a typo) fell out of the map entirely -- even when the station is itself a mesh node that has self-advertised a real GPS position, the same source /api/observers and the Wardriving tab already treat as authoritative. Now checks the observer's own node-table position first (folded into the existing bulk pubkey lookup, no extra query) and only falls back to the IATA table when it has none. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
2f686d40c6
commit
94be933ed5
+33
-13
@@ -1530,9 +1530,10 @@ type PacketPathPoint struct {
|
||||
}
|
||||
|
||||
// PacketPathObserver is the station that produced a given branch's
|
||||
// observation of a packet (see GetPacketPath), positioned from its
|
||||
// configured IATA code the same way the Wardriving tab positions
|
||||
// observers -- not a stored per-observer lat/lon column.
|
||||
// observation of a packet (see GetPacketPath), positioned from its own
|
||||
// self-advertised GPS (the same source /api/observers uses) when known,
|
||||
// falling back to its configured IATA code otherwise -- not a stored
|
||||
// per-observer lat/lon column.
|
||||
type PacketPathObserver struct {
|
||||
Name string `json:"name"`
|
||||
IATA string `json:"iata,omitempty"`
|
||||
@@ -1574,13 +1575,13 @@ func (db *DB) GetPacketPath(hash string) (*PacketPathResponse, error) {
|
||||
}
|
||||
var querySQL string
|
||||
if db.isV3 {
|
||||
querySQL = `SELECT obs.rowid, obs.name, obs.iata, o.path_json, o.resolved_path, o.snr
|
||||
querySQL = `SELECT obs.rowid, obs.id, obs.name, obs.iata, o.path_json, o.resolved_path, o.snr
|
||||
FROM observations o
|
||||
JOIN transmissions t ON t.id = o.transmission_id
|
||||
LEFT JOIN observers obs ON obs.rowid = o.observer_idx
|
||||
WHERE t.hash = ?`
|
||||
} else {
|
||||
querySQL = `SELECT o.observer_id, o.observer_name, NULL, o.path_json, o.resolved_path, o.snr
|
||||
querySQL = `SELECT o.observer_id, o.observer_id, o.observer_name, NULL, o.path_json, o.resolved_path, o.snr
|
||||
FROM observations o
|
||||
JOIN transmissions t ON t.id = o.transmission_id
|
||||
WHERE t.hash = ?`
|
||||
@@ -1592,18 +1593,19 @@ func (db *DB) GetPacketPath(hash string) (*PacketPathResponse, error) {
|
||||
defer rows.Close()
|
||||
|
||||
type obsBranch struct {
|
||||
hops int
|
||||
resolvedPath []*string
|
||||
observerName string
|
||||
observerIATA sql.NullString
|
||||
snr sql.NullFloat64
|
||||
hops int
|
||||
resolvedPath []*string
|
||||
observerName string
|
||||
observerPubkey string
|
||||
observerIATA sql.NullString
|
||||
snr sql.NullFloat64
|
||||
}
|
||||
best := make(map[string]*obsBranch)
|
||||
|
||||
for rows.Next() {
|
||||
var obsKey, obsName, obsIATA, pathJSON, resolvedPathJSON sql.NullString
|
||||
var obsKey, obsPubkey, obsName, obsIATA, pathJSON, resolvedPathJSON sql.NullString
|
||||
var snr sql.NullFloat64
|
||||
if err := rows.Scan(&obsKey, &obsName, &obsIATA, &pathJSON, &resolvedPathJSON, &snr); err != nil {
|
||||
if err := rows.Scan(&obsKey, &obsPubkey, &obsName, &obsIATA, &pathJSON, &resolvedPathJSON, &snr); err != nil {
|
||||
continue
|
||||
}
|
||||
if !pathJSON.Valid {
|
||||
@@ -1628,7 +1630,8 @@ func (db *DB) GetPacketPath(hash string) (*PacketPathResponse, error) {
|
||||
if existing, ok := best[key]; !ok || hops > existing.hops {
|
||||
best[key] = &obsBranch{
|
||||
hops: hops, resolvedPath: resolvedPath,
|
||||
observerName: obsName.String, observerIATA: obsIATA, snr: snr,
|
||||
observerName: obsName.String, observerPubkey: strings.ToLower(strings.TrimSpace(obsPubkey.String)),
|
||||
observerIATA: obsIATA, snr: snr,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1648,6 +1651,13 @@ func (db *DB) GetPacketPath(hash string) (*PacketPathResponse, error) {
|
||||
pubkeySet[*pk] = true
|
||||
}
|
||||
}
|
||||
// An observer is itself a mesh node -- if it has ever self-advertised
|
||||
// a GPS position, that's a more precise fix than its (often manually
|
||||
// typed, sometimes wrong or missing) configured IATA code. Folded
|
||||
// into the same batched lookup below rather than a second query.
|
||||
if b.observerPubkey != "" {
|
||||
pubkeySet[b.observerPubkey] = true
|
||||
}
|
||||
}
|
||||
pubkeys := make([]string, 0, len(pubkeySet))
|
||||
for pk := range pubkeySet {
|
||||
@@ -1713,6 +1723,16 @@ func (db *DB) GetPacketPath(hash string) (*PacketPathResponse, error) {
|
||||
obs := &PacketPathObserver{Name: b.observerName}
|
||||
if b.observerIATA.Valid {
|
||||
obs.IATA = strings.ToUpper(strings.TrimSpace(b.observerIATA.String))
|
||||
}
|
||||
// Prefer the observer's own self-advertised GPS (same source as
|
||||
// /api/observers and the Wardriving tab) over its configured
|
||||
// IATA code -- the IATA table only covers a fixed list of real
|
||||
// airports plus a handful of hand-added local codes, so a
|
||||
// custom/regional code an operator typed in (or a typo) falls
|
||||
// through it even when the node itself knows exactly where it is.
|
||||
if ni, ok := nodeByPK[b.observerPubkey]; ok && ni.lat != nil && ni.lon != nil {
|
||||
obs.Lat, obs.Lon = ni.lat, ni.lon
|
||||
} else if obs.IATA != "" {
|
||||
if coord, ok := iataCoords[obs.IATA]; ok {
|
||||
lat, lon := coord.Lat, coord.Lon
|
||||
obs.Lat, obs.Lon = &lat, &lon
|
||||
|
||||
@@ -677,6 +677,45 @@ func TestGetPacketPath(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetPacketPath_ObserverPositionPrefersOwnGPS covers an observer whose
|
||||
// configured IATA code isn't a real airport (a custom/regional code an
|
||||
// operator typed in, or a typo) and so isn't in the hardcoded iataCoords
|
||||
// table -- but the observer is itself a mesh node that has self-advertised
|
||||
// a real GPS position. That position must be used instead of leaving the
|
||||
// observer unplaced, since it's the same source /api/observers and the
|
||||
// Wardriving tab already treat as authoritative.
|
||||
func TestGetPacketPath_ObserverPositionPrefersOwnGPS(t *testing.T) {
|
||||
db := setupTestDB(t)
|
||||
defer db.Close()
|
||||
|
||||
db.conn.Exec(`INSERT INTO observers (id, name, iata) VALUES ('deadbeefcafe', 'Custom Coded Observer', 'QXV')`)
|
||||
db.conn.Exec(`INSERT INTO nodes (public_key, name, role, lat, lon) VALUES ('deadbeefcafe', 'Custom Coded Observer', 'room', 56.19, 9.6)`)
|
||||
|
||||
db.conn.Exec(`INSERT INTO transmissions (raw_hex, hash, first_seen, route_type, payload_type, decoded_json, channel_hash)
|
||||
VALUES ('AA', 'pathtest00000004', '2026-01-15T10:00:00Z', 1, 5,
|
||||
'{"type":"CHAN","channel":"#ping","text":"ping","sender":"Eve"}', '#ping')`)
|
||||
db.conn.Exec(`INSERT INTO observations (transmission_id, observer_idx, snr, rssi, path_json, timestamp)
|
||||
VALUES (1, 1, 9.0, -88, '[]', 1736935200)`)
|
||||
|
||||
resp, err := db.GetPacketPath("pathtest00000004")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(resp.Branches) != 1 {
|
||||
t.Fatalf("Branches = %+v, want 1", resp.Branches)
|
||||
}
|
||||
obs := resp.Branches[0].Observer
|
||||
if obs == nil {
|
||||
t.Fatalf("Observer = nil, want a populated observer")
|
||||
}
|
||||
if obs.IATA != "QXV" {
|
||||
t.Errorf("Observer.IATA = %q, want QXV (kept even though it's not in iataCoords)", obs.IATA)
|
||||
}
|
||||
if obs.Lat == nil || *obs.Lat != 56.19 || obs.Lon == nil || *obs.Lon != 9.6 {
|
||||
t.Errorf("Observer.Lat/Lon = %v/%v, want the node's own self-advertised GPS (56.19, 9.6), not left nil just because QXV isn't a known airport", obs.Lat, obs.Lon)
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetPacketPath_NoResolvedPath covers a station whose observation
|
||||
// never resolved: it still contributes a branch (hop count from
|
||||
// path_json, so reach is never silently dropped), just with no points.
|
||||
|
||||
@@ -141,7 +141,7 @@ func routeDescriptions() map[string]routeMeta {
|
||||
// Misc
|
||||
"GET /api/resolve-hops": {Summary: "Resolve hop path", Description: "Resolves hash prefixes in a hop path to node names. Returns affinity scores and best candidates.", Tag: "nodes", QueryParams: []paramMeta{{Name: "hops", Description: "Comma-separated hop hash prefixes", Type: "string", Required: true}}},
|
||||
"GET /api/traces/{hash}": {Summary: "Get packet traces", Description: "Returns all observer sightings for a packet hash.", Tag: "packets"},
|
||||
"GET /api/packets/{hash}/path": {Summary: "Get a packet's full geographic flood spread", Description: "Resolves EVERY distinct station that observed a packet to its own branch: hop count (from that station's deepest observation) plus, where resolvable, each relay's name/role/lat/lon in path order and the station's own position (from its configured IATA code, like the Wardriving tab). A station heard more than once (later flood copies via longer routes) contributes only its deepest observation. Lat/lon are null for any hop that has never advertised a GPS position -- callers should draw a gap, not guess. Backs the Channels tab's ping-bot \"View path\" map link.", Tag: "packets",
|
||||
"GET /api/packets/{hash}/path": {Summary: "Get a packet's full geographic flood spread", Description: "Resolves EVERY distinct station that observed a packet to its own branch: hop count (from that station's deepest observation) plus, where resolvable, each relay's name/role/lat/lon in path order and the station's own position (self-advertised GPS when known, same as /api/observers, else its configured IATA code). A station heard more than once (later flood copies via longer routes) contributes only its deepest observation. Lat/lon are null for any hop or observer that has no known position -- callers should draw a gap, not guess. Backs the Channels tab's ping-bot \"View path\" map link.", Tag: "packets",
|
||||
Response: schemaRef("PacketPathResponse")},
|
||||
"GET /api/iata-coords": {Summary: "Get IATA airport coordinates", Description: "Returns lat/lon for known airport codes (used for observer positioning).", Tag: "config"},
|
||||
"GET /api/audio-lab/buckets": {Summary: "Audio lab frequency buckets", Description: "Returns frequency bucket data for audio analysis.", Tag: "analytics"},
|
||||
@@ -346,7 +346,7 @@ func componentSchemas() map[string]interface{} {
|
||||
},
|
||||
"PacketPathObserver": map[string]interface{}{
|
||||
"type": "object",
|
||||
"description": "The station that produced a given branch's observation of a packet path, positioned from its configured IATA code.",
|
||||
"description": "The station that produced a given branch's observation of a packet path, positioned from its own self-advertised GPS when known (same source as /api/observers), else its configured IATA code.",
|
||||
"properties": map[string]interface{}{
|
||||
"name": str("Observer display name."),
|
||||
"iata": str("Observer's configured IATA airport code, when set."),
|
||||
|
||||
Reference in New Issue
Block a user