mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-06-06 15:11:37 +00:00
2329639f45
@ ## What this PR does Implements region-scoped transport-route packet tracking with two sub-features: ### Feature 1 — Scope statistics (`scope_name`) - At ingest, transport-route packets (route_type 0/3) with Code1 != `0000` are HMAC-matched against configured `hashRegions` keys (mirroring the `hashChannels` pattern). Matched region name (or `""` for unknown) stored in new `transmissions.scope_name` column via migration `scope_name_v1`. - New `GET /api/scope-stats?window=` endpoint (1h/24h/7d, 30s server-side TTL) returning transport totals, scoped/unscoped counts, per-region breakdown, and time-series. - New **Scopes** tab in Analytics with summary cards, per-region table, and two-line SVG chart. Auto-refreshes every 60s. ### Feature 2 — Node default scope (`default_scope`) - Per-node `default_scope` column on `nodes`/`inactive_nodes` (migration `nodes_default_scope_v1`) tracks the most recently matched region for each node, derived from transport-scoped ADVERT packets. - `GET /api/nodes` response includes `default_scope` field when column is present. - Node detail panel displays the default scope badge. - Async startup backfill (`BackfillDefaultScopeAsync`) populates the column for nodes with pre-existing ADVERT data. ### Config Add `hashRegions` to `config.json` (see `config.example.json`). One entry per region name (with or without leading `#`). @ --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: Kpa-clawbot <kpaclawbot@outlook.com> Co-authored-by: openclaw-bot <bot@openclaw.local>
95 lines
2.7 KiB
Go
95 lines
2.7 KiB
Go
package main
|
|
|
|
// Tests for #1143: ingestor must populate transmissions.from_pubkey at
|
|
// write time (cheap — already parsing decoded_json) so attribution queries
|
|
// don't rely on JSON substring matches.
|
|
|
|
import (
|
|
"database/sql"
|
|
"testing"
|
|
)
|
|
|
|
func TestInsertTransmission_FromPubkeyPopulatedForAdvert(t *testing.T) {
|
|
s, err := OpenStore(tempDBPath(t))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer s.Close()
|
|
|
|
const pk = "f7181c468dfe7c55aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
|
data := &PacketData{
|
|
RawHex: "AABBCC",
|
|
Timestamp: "2026-03-25T00:00:00Z",
|
|
ObserverID: "obs1",
|
|
Hash: "advert_hash_1143",
|
|
RouteType: 1,
|
|
PayloadType: 4, // ADVERT
|
|
PayloadVersion: 0,
|
|
PathJSON: "[]",
|
|
DecodedJSON: `{"type":"ADVERT","pubKey":"` + pk + `","name":"X"}`,
|
|
FromPubkey: pk,
|
|
}
|
|
if _, err := s.InsertTransmission(data); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
var got sql.NullString
|
|
s.db.QueryRow("SELECT from_pubkey FROM transmissions WHERE hash = ?", data.Hash).Scan(&got)
|
|
if !got.Valid || got.String != pk {
|
|
t.Fatalf("from_pubkey = %v (valid=%v), want %q", got.String, got.Valid, pk)
|
|
}
|
|
}
|
|
|
|
func TestInsertTransmission_FromPubkeyNullForNonAdvert(t *testing.T) {
|
|
s, err := OpenStore(tempDBPath(t))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer s.Close()
|
|
|
|
data := &PacketData{
|
|
RawHex: "AA",
|
|
Timestamp: "2026-03-25T00:00:00Z",
|
|
ObserverID: "obs1",
|
|
Hash: "txt_hash_1143",
|
|
RouteType: 1,
|
|
PayloadType: 2, // TXT_MSG
|
|
PayloadVersion: 0,
|
|
PathJSON: "[]",
|
|
DecodedJSON: `{"type":"TXT_MSG"}`,
|
|
// FromPubkey deliberately empty — non-ADVERTs don't carry one.
|
|
}
|
|
if _, err := s.InsertTransmission(data); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
var got sql.NullString
|
|
s.db.QueryRow("SELECT from_pubkey FROM transmissions WHERE hash = ?", data.Hash).Scan(&got)
|
|
if got.Valid {
|
|
t.Fatalf("from_pubkey for non-ADVERT must be NULL, got %q", got.String)
|
|
}
|
|
}
|
|
|
|
func TestBuildPacketData_PopulatesFromPubkey(t *testing.T) {
|
|
const pk = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"
|
|
msg := &MQTTPacketMessage{Raw: "AA", Origin: "obs"}
|
|
decoded := &DecodedPacket{
|
|
Header: Header{PayloadType: PayloadADVERT},
|
|
Payload: Payload{Type: "ADVERT", PubKey: pk},
|
|
}
|
|
pd := BuildPacketData(msg, decoded, "obs", "", nil)
|
|
if pd.FromPubkey != pk {
|
|
t.Fatalf("BuildPacketData FromPubkey = %q, want %q", pd.FromPubkey, pk)
|
|
}
|
|
|
|
// Non-ADVERT: must not carry a pubkey.
|
|
decoded2 := &DecodedPacket{
|
|
Header: Header{PayloadType: 2},
|
|
Payload: Payload{Type: "TXT_MSG"},
|
|
}
|
|
pd2 := BuildPacketData(msg, decoded2, "obs", "", nil)
|
|
if pd2.FromPubkey != "" {
|
|
t.Fatalf("BuildPacketData FromPubkey for non-ADVERT = %q, want empty", pd2.FromPubkey)
|
|
}
|
|
}
|