fix(go): audio-lab/buckets use in-memory store, match Node.js behavior

Use s.store (in-memory PacketStore) instead of direct packets_v SQL query,
matching how the Node.js handler iterates pktStore.packets. This fixes the
endpoint returning empty buckets when packets_v view is missing or the DB
query fails silently.

Key changes:
- Group by decoded_json.type first, fall back to payloadTypeNames
- Evenly-spaced sampling (up to 8 per type) sorted by raw_hex length
- Use actual ObservationCount instead of hardcoded 1
- Reuse payloadTypeNames from store.go instead of duplicating
- Retain DB fallback path when store is nil

fixes #170

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Kpa-clawbot
2026-03-27 15:47:32 -07:00
parent f5ea4d5a87
commit 30bcfff45b
+81 -29
View File
@@ -1993,39 +1993,91 @@ func (s *Server) handleIATACoords(w http.ResponseWriter, r *http.Request) {
}
func (s *Server) handleAudioLabBuckets(w http.ResponseWriter, r *http.Request) {
// Query representative packets by type
ptSQL := `SELECT payload_type, id, raw_hex, hash, decoded_json, path_json, observer_id, timestamp
FROM (
SELECT *, ROW_NUMBER() OVER (PARTITION BY payload_type ORDER BY length(raw_hex)) as rn
FROM packets_v WHERE raw_hex IS NOT NULL
) sub WHERE rn <= 8`
rows, err := s.db.conn.Query(ptSQL)
if err != nil {
writeJSON(w, AudioLabBucketsResponse{Buckets: map[string][]AudioLabPacket{}})
return
}
defer rows.Close()
ptNames := map[int]string{0: "REQ", 1: "RESPONSE", 2: "TXT_MSG", 3: "ACK", 4: "ADVERT", 5: "GRP_TXT", 7: "ANON_REQ", 8: "PATH", 9: "TRACE", 11: "CONTROL"}
buckets := map[string][]AudioLabPacket{}
for rows.Next() {
var pt, id int
var rawHex, hash, decodedJSON, pathJSON, obsID, ts sql.NullString
rows.Scan(&pt, &id, &rawHex, &hash, &decodedJSON, &pathJSON, &obsID, &ts)
typeName := ptNames[pt]
if typeName == "" {
typeName = "UNKNOWN"
if s.store != nil {
// Use in-memory store (matches Node.js pktStore.packets approach)
s.store.mu.RLock()
byType := map[string][]*StoreTx{}
for _, tx := range s.store.packets {
if tx.RawHex == "" {
continue
}
typeName := "UNKNOWN"
if tx.DecodedJSON != "" {
var d map[string]interface{}
if err := json.Unmarshal([]byte(tx.DecodedJSON), &d); err == nil {
if t, ok := d["type"].(string); ok && t != "" {
typeName = t
}
}
}
if typeName == "UNKNOWN" && tx.PayloadType != nil {
if name, ok := payloadTypeNames[*tx.PayloadType]; ok {
typeName = name
}
}
byType[typeName] = append(byType[typeName], tx)
}
if _, ok := buckets[typeName]; !ok {
buckets[typeName] = make([]AudioLabPacket, 0)
s.store.mu.RUnlock()
for typeName, pkts := range byType {
sort.Slice(pkts, func(i, j int) bool {
return len(pkts[i].RawHex) < len(pkts[j].RawHex)
})
count := min(8, len(pkts))
picked := make([]AudioLabPacket, 0, count)
for i := 0; i < count; i++ {
idx := (i * len(pkts)) / count
tx := pkts[idx]
pt := 0
if tx.PayloadType != nil {
pt = *tx.PayloadType
}
picked = append(picked, AudioLabPacket{
Hash: strOrNil(tx.Hash),
RawHex: strOrNil(tx.RawHex),
DecodedJSON: strOrNil(tx.DecodedJSON),
ObservationCount: max(tx.ObservationCount, 1),
PayloadType: pt,
PathJSON: strOrNil(tx.PathJSON),
ObserverID: strOrNil(tx.ObserverID),
Timestamp: strOrNil(tx.FirstSeen),
})
}
buckets[typeName] = picked
}
} else {
// Fallback: direct DB query when store is not loaded
ptSQL := `SELECT payload_type, id, raw_hex, hash, decoded_json, path_json, observer_id, timestamp
FROM (
SELECT *, ROW_NUMBER() OVER (PARTITION BY payload_type ORDER BY length(raw_hex)) as rn
FROM packets_v WHERE raw_hex IS NOT NULL
) sub WHERE rn <= 8`
rows, err := s.db.conn.Query(ptSQL)
if err != nil {
writeJSON(w, AudioLabBucketsResponse{Buckets: buckets})
return
}
defer rows.Close()
for rows.Next() {
var pt, id int
var rawHex, hash, decodedJSON, pathJSON, obsID, ts sql.NullString
rows.Scan(&pt, &id, &rawHex, &hash, &decodedJSON, &pathJSON, &obsID, &ts)
typeName := payloadTypeNames[pt]
if typeName == "" {
typeName = "UNKNOWN"
}
buckets[typeName] = append(buckets[typeName], AudioLabPacket{
Hash: nullStr(hash), RawHex: nullStr(rawHex),
DecodedJSON: nullStr(decodedJSON), ObservationCount: 1,
PayloadType: pt, PathJSON: nullStr(pathJSON),
ObserverID: nullStr(obsID), Timestamp: nullStr(ts),
})
}
buckets[typeName] = append(buckets[typeName], AudioLabPacket{
Hash: nullStr(hash), RawHex: nullStr(rawHex),
DecodedJSON: nullStr(decodedJSON), ObservationCount: 1,
PayloadType: pt, PathJSON: nullStr(pathJSON),
ObserverID: nullStr(obsID), Timestamp: nullStr(ts),
})
}
writeJSON(w, AudioLabBucketsResponse{Buckets: buckets})
}