mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-10-23 16:52:42 +00:00
## Summary Switches channel API endpoints to query SQLite instead of the in-memory packet store, giving users access to the full message history. Implements #725 (M1 only — DB-backed channel messages). Does NOT close #725 — M2-M5 (custom channels, PSK, persistence, retroactive decryption) remain. ## Problem Channel endpoints (`/api/channels`, `/api/channels/{hash}/messages`) preferred the in-memory packet store when available. The store is bounded by `packetStore.maxMemoryMB` — typically showing only recent messages. The SQLite database has the complete history (weeks/months of channel messages) but was only used as a fallback when the store was nil (never in production). ## Fix Reversed the preference order: DB first, in-memory store fallback. Region filtering added to the DB path. Co-authored-by: you <you@example.com>
This commit is contained in:
1 parent
c233c14156
commit
71be54f085
3 files changed
+120
-15
No files matched your search
+42
-2
@@ -1153,8 +1153,48 @@ func (db *DB) GetTraces(hash string) ([]map[string]interface{}, error) {
|
||||
// Queries transmissions directly (not a VIEW) to avoid observation-level
|
||||
// duplicates that could cause stale lastMessage when an older message has
|
||||
// a later re-observation timestamp.
|
||||
func (db *DB) GetChannels() ([]map[string]interface{}, error) {
|
||||
rows, err := db.conn.Query(`SELECT decoded_json, first_seen FROM transmissions WHERE payload_type = 5 ORDER BY first_seen ASC`)
|
||||
func (db *DB) GetChannels(region ...string) ([]map[string]interface{}, error) {
|
||||
regionParam := ""
|
||||
if len(region) > 0 {
|
||||
regionParam = region[0]
|
||||
}
|
||||
regionCodes := normalizeRegionCodes(regionParam)
|
||||
|
||||
var querySQL string
|
||||
args := make([]interface{}, 0, len(regionCodes))
|
||||
|
||||
if len(regionCodes) > 0 {
|
||||
placeholders := make([]string, len(regionCodes))
|
||||
for i, code := range regionCodes {
|
||||
placeholders[i] = "?"
|
||||
args = append(args, code)
|
||||
}
|
||||
regionPlaceholder := strings.Join(placeholders, ",")
|
||||
if db.isV3 {
|
||||
querySQL = fmt.Sprintf(`SELECT DISTINCT t.decoded_json, t.first_seen
|
||||
FROM transmissions t
|
||||
JOIN observations o ON o.transmission_id = t.id
|
||||
LEFT JOIN observers obs ON obs.rowid = o.observer_idx
|
||||
WHERE t.payload_type = 5
|
||||
AND obs.rowid IS NOT NULL AND UPPER(TRIM(obs.iata)) IN (%s)
|
||||
ORDER BY t.first_seen ASC`, regionPlaceholder)
|
||||
} else {
|
||||
querySQL = fmt.Sprintf(`SELECT DISTINCT t.decoded_json, t.first_seen
|
||||
FROM transmissions t
|
||||
JOIN observations o ON o.transmission_id = t.id
|
||||
WHERE t.payload_type = 5
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM observers obs
|
||||
WHERE obs.id = o.observer_id
|
||||
AND UPPER(TRIM(obs.iata)) IN (%s)
|
||||
)
|
||||
ORDER BY t.first_seen ASC`, regionPlaceholder)
|
||||
}
|
||||
} else {
|
||||
querySQL = `SELECT decoded_json, first_seen FROM transmissions WHERE payload_type = 5 ORDER BY first_seen ASC`
|
||||
}
|
||||
|
||||
rows, err := db.conn.Query(querySQL, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -1504,6 +1504,61 @@ func TestGetChannelsStaleMessage(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetChannelsRegionFiltering(t *testing.T) {
|
||||
db := setupTestDB(t)
|
||||
defer db.Close()
|
||||
|
||||
db.conn.Exec(`INSERT INTO observers (id, name, iata) VALUES ('obs1', 'Observer1', 'SJC')`)
|
||||
db.conn.Exec(`INSERT INTO observers (id, name, iata) VALUES ('obs2', 'Observer2', 'SFO')`)
|
||||
|
||||
// Channel message seen only in SJC
|
||||
db.conn.Exec(`INSERT INTO transmissions (raw_hex, hash, first_seen, route_type, payload_type, decoded_json)
|
||||
VALUES ('AA', 'hash1', '2026-01-15T10:00:00Z', 1, 5,
|
||||
'{"type":"CHAN","channel":"#sjc-only","text":"Alice: Hello SJC","sender":"Alice"}')`)
|
||||
db.conn.Exec(`INSERT INTO observations (transmission_id, observer_idx, snr, rssi, timestamp)
|
||||
VALUES (1, 1, 12.0, -90, 1736935200)`)
|
||||
|
||||
// Channel message seen only in SFO
|
||||
db.conn.Exec(`INSERT INTO transmissions (raw_hex, hash, first_seen, route_type, payload_type, decoded_json)
|
||||
VALUES ('BB', 'hash2', '2026-01-15T10:05:00Z', 1, 5,
|
||||
'{"type":"CHAN","channel":"#sfo-only","text":"Bob: Hello SFO","sender":"Bob"}')`)
|
||||
db.conn.Exec(`INSERT INTO observations (transmission_id, observer_idx, snr, rssi, timestamp)
|
||||
VALUES (2, 2, 14.0, -88, 1736935500)`)
|
||||
|
||||
// No region filter — both channels
|
||||
all, err := db.GetChannels()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(all) != 2 {
|
||||
t.Fatalf("expected 2 channels without region filter, got %d", len(all))
|
||||
}
|
||||
|
||||
// Filter SJC — only #sjc-only
|
||||
sjc, err := db.GetChannels("SJC")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(sjc) != 1 {
|
||||
t.Fatalf("expected 1 channel for SJC, got %d", len(sjc))
|
||||
}
|
||||
if sjc[0]["name"] != "#sjc-only" {
|
||||
t.Errorf("expected channel '#sjc-only', got %q", sjc[0]["name"])
|
||||
}
|
||||
|
||||
// Filter SFO — only #sfo-only
|
||||
sfo, err := db.GetChannels("SFO")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(sfo) != 1 {
|
||||
t.Fatalf("expected 1 channel for SFO, got %d", len(sfo))
|
||||
}
|
||||
if sfo[0]["name"] != "#sfo-only" {
|
||||
t.Errorf("expected channel '#sfo-only', got %q", sfo[0]["name"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestNodeTelemetryFields(t *testing.T) {
|
||||
db := setupTestDB(t)
|
||||
defer db.Close()
|
||||
|
||||
+23
-13
@@ -1655,18 +1655,23 @@ func (s *Server) handleResolveHops(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func (s *Server) handleChannels(w http.ResponseWriter, r *http.Request) {
|
||||
region := r.URL.Query().Get("region")
|
||||
// Prefer DB for full history (in-memory store has limited retention)
|
||||
if s.db != nil {
|
||||
channels, err := s.db.GetChannels(region)
|
||||
if err != nil {
|
||||
writeError(w, 500, err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, ChannelListResponse{Channels: channels})
|
||||
return
|
||||
}
|
||||
if s.store != nil {
|
||||
region := r.URL.Query().Get("region")
|
||||
channels := s.store.GetChannels(region)
|
||||
writeJSON(w, ChannelListResponse{Channels: channels})
|
||||
return
|
||||
}
|
||||
channels, err := s.db.GetChannels()
|
||||
if err != nil {
|
||||
writeError(w, 500, err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, ChannelListResponse{Channels: channels})
|
||||
writeJSON(w, ChannelListResponse{Channels: []map[string]interface{}{}})
|
||||
}
|
||||
|
||||
func (s *Server) handleChannelMessages(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -1674,17 +1679,22 @@ func (s *Server) handleChannelMessages(w http.ResponseWriter, r *http.Request) {
|
||||
limit := queryInt(r, "limit", 100)
|
||||
offset := queryInt(r, "offset", 0)
|
||||
region := r.URL.Query().Get("region")
|
||||
// Prefer DB for full history (in-memory store has limited retention)
|
||||
if s.db != nil {
|
||||
messages, total, err := s.db.GetChannelMessages(hash, limit, offset, region)
|
||||
if err != nil {
|
||||
writeError(w, 500, err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, ChannelMessagesResponse{Messages: messages, Total: total})
|
||||
return
|
||||
}
|
||||
if s.store != nil {
|
||||
messages, total := s.store.GetChannelMessages(hash, limit, offset, region)
|
||||
writeJSON(w, ChannelMessagesResponse{Messages: messages, Total: total})
|
||||
return
|
||||
}
|
||||
messages, total, err := s.db.GetChannelMessages(hash, limit, offset, region)
|
||||
if err != nil {
|
||||
writeError(w, 500, err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, ChannelMessagesResponse{Messages: messages, Total: total})
|
||||
writeJSON(w, ChannelMessagesResponse{Messages: []map[string]interface{}{}, Total: 0})
|
||||
}
|
||||
|
||||
func (s *Server) handleObservers(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
Reference in new issue
Block a user