diff --git a/README.md b/README.md index 70ddf06..6352b6d 100644 --- a/README.md +++ b/README.md @@ -421,10 +421,11 @@ For paginated responses use the generic page wrapper: - [x] Parse payloads (that we can decrypt) into DB and return with packet details - [x] Propagation time calculation +- [x] Trace route resolution via path hashes (resolvedRoute on packet detail) ### In progress / next -- [ ] Routes and traces endpoints +- [ ] Dedicated routes and traces endpoints (see issue #32) ### Future diff --git a/db/store.go b/db/store.go index a9f9702..cc213d8 100644 --- a/db/store.go +++ b/db/store.go @@ -1289,6 +1289,84 @@ func (s *Store) GetPacket(ctx context.Context, packetHash []byte) (*api.Packet, } p.Observations = append(p.Observations, obs) } + if row.PayloadType == 9 && len(obsRows) > 0 { + var tracePayload struct { + PathHashes []string `json:"pathHashes"` + Flags byte `json:"flags"` + } + if err := json.Unmarshal(row.ParsedPayload, &tracePayload); err == nil && len(tracePayload.PathHashes) > 0 { + hashSize := int(1 << (tracePayload.Flags & 0x03)) + hashes := make([][]byte, 0, len(tracePayload.PathHashes)) + for _, h := range tracePayload.PathHashes { + b, err := hex.DecodeString(h) + if err == nil { + hashes = append(hashes, b) + } + } + + // collect unique IATAs from observations + seenIATAs := make(map[string]struct{}) + for _, v := range obsRows { + seenIATAs[v.Iata] = struct{}{} + } + + // merge results across all IATAs — best confidence per hop wins + // confidence priority: high > ambiguous > none + type hopResult struct { + confidence string + entries []api.ResolvedPathEntry + } + merged := make([]hopResult, len(hashes)) + for i := range merged { + merged[i] = hopResult{confidence: "none"} + } + + confidenceRank := map[string]int{"none": 0, "ambiguous": 1, "high": 2} + + for iata := range seenIATAs { + resolved, err := s.ResolvePathHashes(ctx, iata, hashes) + if err != nil { + log.Printf("store: trace route resolution failed for iata=%s: %v", iata, err) + continue + } + for i, hash := range hashes { + key := hex.EncodeToString(hash[:hashSize]) + entries := resolved[key] + var confidence string + switch len(entries) { + case 0: + confidence = "none" + case 1: + confidence = "high" + default: + confidence = "ambiguous" + } + if confidenceRank[confidence] > confidenceRank[merged[i].confidence] { + merged[i] = hopResult{confidence: confidence, entries: entries} + } + } + } + + route := make([]api.ResolvedHop, 0, len(hashes)) + for _, hr := range merged { + hop := api.ResolvedHop{ + Confidence: hr.confidence, + Nodes: make([]api.ResolvedNode, 0, len(hr.entries)), + } + for _, e := range hr.entries { + hop.Nodes = append(hop.Nodes, api.ResolvedNode{ + ID: e.NodeID, + Name: e.Name, + Latitude: e.Latitude, + Longitude: e.Longitude, + PublicKey: hex.EncodeToString(e.PublicKey), + }) + } + route = append(route, hop) + } + p.ResolvedRoute = route + } + } return p, nil } diff --git a/docs/docs.go b/docs/docs.go index 31b0d8f..aa8039e 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -937,7 +937,7 @@ const docTemplate = `{ "tags": [ "Packets" ], - "summary": "Get full packet detail", + "summary": "Get full packet detail. For trace packets (payloadType=9), includes resolvedRoute", "parameters": [ { "type": "string", @@ -1846,6 +1846,12 @@ const docTemplate = `{ "rawPayload": { "type": "string" }, + "resolvedRoute": { + "type": "array", + "items": { + "$ref": "#/definitions/github_com_MeshCore-Tower_tower-server_internal_api.ResolvedHop" + } + }, "scope": { "type": "string" }, diff --git a/docs/swagger.json b/docs/swagger.json index 27a8dfa..0921096 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -935,7 +935,7 @@ "tags": [ "Packets" ], - "summary": "Get full packet detail", + "summary": "Get full packet detail. For trace packets (payloadType=9), includes resolvedRoute", "parameters": [ { "type": "string", @@ -1844,6 +1844,12 @@ "rawPayload": { "type": "string" }, + "resolvedRoute": { + "type": "array", + "items": { + "$ref": "#/definitions/github_com_MeshCore-Tower_tower-server_internal_api.ResolvedHop" + } + }, "scope": { "type": "string" }, diff --git a/docs/swagger.yaml b/docs/swagger.yaml index d693f55..a40e326 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -363,6 +363,10 @@ definitions: type: array rawPayload: type: string + resolvedRoute: + items: + $ref: '#/definitions/github_com_MeshCore-Tower_tower-server_internal_api.ResolvedHop' + type: array scope: type: string transportCodes: @@ -1310,7 +1314,8 @@ paths: description: Internal Server Error schema: $ref: '#/definitions/internal_api_handlers.APIError' - summary: Get full packet detail + summary: Get full packet detail. For trace packets (payloadType=9), includes + resolvedRoute tags: - Packets /regions: diff --git a/internal/api/handlers/packets.go b/internal/api/handlers/packets.go index 7f051c6..7eeed93 100644 --- a/internal/api/handlers/packets.go +++ b/internal/api/handlers/packets.go @@ -123,7 +123,7 @@ func listPackets(reader api.Reader) http.HandlerFunc { // getPacket godoc // -// @Summary Get full packet detail +// @Summary Get full packet detail. For trace packets (payloadType=9), includes resolvedRoute // @Tags Packets // @Produce json // @Param packetHash path string true "Packet hash (hex)" diff --git a/internal/api/reader.go b/internal/api/reader.go index e9798d7..1e6a5d6 100644 --- a/internal/api/reader.go +++ b/internal/api/reader.go @@ -176,6 +176,7 @@ type Packet struct { LastHeardAt int64 `json:"lastHeardAt"` FirstToLastMs int64 `json:"firstToLastMs"` ObservationCount int32 `json:"observationCount"` + ResolvedRoute []ResolvedHop `json:"resolvedRoute,omitempty"` Observations []PacketObservationDetail `json:"observations"` }