mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-06-03 22:51:18 +00:00
9d3dd8df0a
## Summary Fixes #1345 — the packets page shows "no recent activity" while MQTT ingest is healthy because the default `/api/packets` query was `ORDER BY first_seen DESC`, and PR #1233 redefined `first_seen` as the observer's radio receive time (rxTime). When an observer buffers offline and uploads hours later, its packets land with hours-old `first_seen` values; older-ingested packets with fresher rxTime then crowd the top of the list and the visually freshest activity disappears. ## Fix Switch the default ordering to `t.id DESC` (ingest order) on `/api/packets` and the closely-related endpoints. `id` is monotonic with ingest time and immune to buffered uploads. Endpoints changed (all use the same fix for the same reason): | Path | Function | File | |------|----------|------| | `GET /api/packets` (default) | `DB.QueryPackets`, `Store.QueryPackets` | `cmd/server/db.go`, `cmd/server/store.go` | | `GET /api/packets?nodes=…` | `DB.QueryMultiNodePackets`, `Store.QueryMultiNodePackets` | same | | Node detail "recent transmissions" | `DB.GetRecentTransmissionsForNode` | `cmd/server/db.go` | ## `since=` semantic — preserved `since=` still filters by `first_seen` (RFC3339 path uses the observations.timestamp subquery), i.e. "packets the network received since X." Buffered uploads of older packets are still excluded from a `since=15m` view even if they were ingested in the last 15 minutes. Only the **display order** changes; filtering by receive time is unchanged. ## Audit — NOT changed - `Store.QueryGroupedPackets` already sorts by `LatestSeen` (max observation timestamp), which is correct for the grouped view and immune to the buffered-upload regression. - `GetChannelMessages` and channel `sample_json` subqueries keep `first_seen DESC` — channel message chronology is meaningful for message UX; if buffered uploads become a problem here too it's a separate UX call (out of scope for #1345). - `s.packets` insertion ordering (Load + ingest) — untouched. The fix sorts at query time so we don't perturb `oldestLoaded` invariants. ## Tests — TDD red → green - Red: `508f4371` adds `cmd/server/packets_order_test.go` with two cases — order assertion (failed on master with `[fresh, buffered]`) and since-filter semantic (RFC3339 path uses observation timestamps). - Green: `0fd685e7` switches the SQL + in-memory ordering. Tests pass; full `cmd/server` suite green locally (44s). ## Out of scope - Re-thinking #1233's first_seen semantics - Adding a UI sort toggle (issue's option 2) - Channel-message page ordering ## Preflight Clean (`bash ~/.openclaw/skills/pr-preflight/scripts/run-all.sh origin/master`). --------- Co-authored-by: openclaw-bot <bot@openclaw.local>
115 lines
4.7 KiB
Go
115 lines
4.7 KiB
Go
package main
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// TestQueryPacketsOrdersByIngestID is the regression test for issue #1345.
|
|
//
|
|
// PR #1233 changed `first_seen` to be the observer's receive time (rxTime),
|
|
// not the moment the server ingested the row. When an observer buffers
|
|
// offline and uploads hours later, its packets land with old first_seen
|
|
// values. The /api/packets handler previously ordered by
|
|
// `first_seen DESC`, so buffered uploads with old rxTime appeared at the
|
|
// bottom while older-ingested packets with newer rxTime took the top —
|
|
// users on the packets page saw "no recent activity" even though MQTT
|
|
// ingest was active.
|
|
//
|
|
// Fix: default ordering for /api/packets is `t.id DESC` (ingest order).
|
|
// This test inserts two rows where row order by id and order by
|
|
// first_seen DISAGREE, then asserts the result is ordered by id DESC.
|
|
func TestQueryPacketsOrdersByIngestID(t *testing.T) {
|
|
db := setupTestDB(t)
|
|
defer db.Close()
|
|
|
|
now := time.Now().UTC()
|
|
// Row A: ingested FIRST (lower id), rxTime "newer" (fresher first_seen)
|
|
freshFirstSeen := now.Add(-1 * time.Hour).Format(time.RFC3339)
|
|
// Row B: ingested SECOND (higher id), rxTime "older" — simulating a
|
|
// buffered observer upload that arrived after row A but contains a
|
|
// packet the radio received hours earlier.
|
|
bufferedFirstSeen := now.Add(-6 * time.Hour).Format(time.RFC3339)
|
|
|
|
if _, err := db.conn.Exec(`INSERT INTO transmissions (raw_hex, hash, first_seen, payload_type)
|
|
VALUES ('AA', 'hashfresh00000001', ?, 4)`, freshFirstSeen); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := db.conn.Exec(`INSERT INTO transmissions (raw_hex, hash, first_seen, payload_type)
|
|
VALUES ('BB', 'hashbuffered00002', ?, 4)`, bufferedFirstSeen); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
result, err := db.QueryPackets(PacketQuery{Limit: 50, Order: "DESC"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(result.Packets) != 2 {
|
|
t.Fatalf("expected 2 packets, got %d", len(result.Packets))
|
|
}
|
|
// With first_seen DESC (the bug), the order would be [fresh, buffered]
|
|
// because the fresh row has the newer rxTime. With the fix (id DESC),
|
|
// order is [buffered, fresh] because the buffered row was ingested
|
|
// second and has the higher id.
|
|
first, _ := result.Packets[0]["hash"].(string)
|
|
second, _ := result.Packets[1]["hash"].(string)
|
|
if first != "hashbuffered00002" || second != "hashfresh00000001" {
|
|
t.Errorf("expected order [buffered, fresh] by ingest id DESC, got [%s, %s]",
|
|
first, second)
|
|
}
|
|
}
|
|
|
|
// TestQueryPacketsSinceFilterUsesFirstSeen documents the chosen semantic for
|
|
// the `since=` query param: it still filters by `first_seen` (radio receive
|
|
// time), NOT by ingest time. Rationale: callers using `since=` expect
|
|
// "packets the network received since X" — buffered uploads of older
|
|
// packets should still be EXCLUDED from a `since=15min` view even if
|
|
// they were ingested in the last 15 minutes. Display order is by ingest
|
|
// id (issue #1345 fix); filter semantic is unchanged.
|
|
func TestQueryPacketsSinceFilterUsesFirstSeen(t *testing.T) {
|
|
db := setupTestDB(t)
|
|
defer db.Close()
|
|
|
|
now := time.Now().UTC()
|
|
recent := now.Add(-30 * time.Minute).Format(time.RFC3339)
|
|
old := now.Add(-6 * time.Hour).Format(time.RFC3339)
|
|
sinceCutoff := now.Add(-1 * time.Hour).Format(time.RFC3339)
|
|
recentEpoch := now.Add(-30 * time.Minute).Unix()
|
|
oldEpoch := now.Add(-6 * time.Hour).Unix()
|
|
|
|
if _, err := db.conn.Exec(`INSERT INTO observers (id, name, last_seen, first_seen, packet_count)
|
|
VALUES ('obs1', 'Obs1', ?, ?, 1)`, recent, recent); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := db.conn.Exec(`INSERT INTO transmissions (raw_hex, hash, first_seen, payload_type)
|
|
VALUES ('AA', 'recentrx00000001', ?, 4)`, recent); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// Buffered upload — ingested SECOND, but rxTime is 6h ago.
|
|
if _, err := db.conn.Exec(`INSERT INTO transmissions (raw_hex, hash, first_seen, payload_type)
|
|
VALUES ('BB', 'oldrxbuffered001', ?, 4)`, old); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := db.conn.Exec(`INSERT INTO observations (transmission_id, observer_idx, snr, rssi, path_json, timestamp)
|
|
VALUES (1, 1, 10, -90, '[]', ?)`, recentEpoch); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := db.conn.Exec(`INSERT INTO observations (transmission_id, observer_idx, snr, rssi, path_json, timestamp)
|
|
VALUES (2, 1, 10, -90, '[]', ?)`, oldEpoch); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
result, err := db.QueryPackets(PacketQuery{Limit: 50, Order: "DESC", Since: sinceCutoff})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(result.Packets) != 1 {
|
|
t.Fatalf("since= should filter by first_seen (rxTime); expected 1 packet, got %d",
|
|
len(result.Packets))
|
|
}
|
|
h, _ := result.Packets[0]["hash"].(string)
|
|
if h != "recentrx00000001" {
|
|
t.Errorf("expected the rxTime-recent packet, got %s", h)
|
|
}
|
|
}
|