mirror of
https://github.com/MeshCore-Beacon/beacon-server.git
synced 2026-09-01 16:48:19 +00:00
@@ -0,0 +1,32 @@
|
||||
-- Copyright 2026 Beacon Contributors
|
||||
-- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
-- Splits mv_top_advertisers_by_iata's advert_count into flood- and direct-routed counts,
|
||||
-- alongside the existing combined total, so /stats/top-advertisers can report how a node is
|
||||
-- actually being heard rather than only a single blended number.
|
||||
--
|
||||
-- Flood = route_type 0 (transport_flood) or 1 (flood) -- broadcast, no prior path needed.
|
||||
-- Direct = route_type 2 (direct) or 3 (transport_direct) -- routed along a known path.
|
||||
|
||||
DROP MATERIALIZED VIEW mv_top_advertisers_by_iata;
|
||||
|
||||
CREATE MATERIALIZED VIEW mv_top_advertisers_by_iata AS
|
||||
SELECT
|
||||
po.iata,
|
||||
n.id AS node_id,
|
||||
n.name,
|
||||
n.node_type,
|
||||
date_trunc('hour', po.heard_at)::timestamptz AS bucket,
|
||||
COUNT(DISTINCT p.packet_hash) AS advert_count,
|
||||
COUNT(DISTINCT p.packet_hash) FILTER (WHERE p.route_type IN (0, 1)) AS flood_advert_count,
|
||||
COUNT(DISTINCT p.packet_hash) FILTER (WHERE p.route_type IN (2, 3)) AS direct_advert_count,
|
||||
MAX(po.heard_at) AS last_heard
|
||||
FROM packets p
|
||||
JOIN packet_observations po ON po.packet_hash = p.packet_hash
|
||||
JOIN nodes n ON n.public_key = p.origin_pubkey
|
||||
WHERE p.payload_type = 4 -- ADVERT
|
||||
AND po.heard_at > NOW() - INTERVAL '30 days'
|
||||
GROUP BY po.iata, n.id, n.name, n.node_type, date_trunc('hour', po.heard_at);
|
||||
|
||||
CREATE UNIQUE INDEX idx_mv_top_advertisers
|
||||
ON mv_top_advertisers_by_iata(iata, node_id, bucket);
|
||||
@@ -902,6 +902,8 @@ SELECT
|
||||
name,
|
||||
node_type,
|
||||
COALESCE(SUM(advert_count), 0)::bigint AS advert_count,
|
||||
COALESCE(SUM(flood_advert_count), 0)::bigint AS flood_advert_count,
|
||||
COALESCE(SUM(direct_advert_count), 0)::bigint AS direct_advert_count,
|
||||
MAX(last_heard)::timestamptz AS last_heard,
|
||||
COALESCE(MAX(iata), '')::bpchar AS iata
|
||||
FROM mv_top_advertisers_by_iata
|
||||
|
||||
+9
-7
@@ -90,13 +90,15 @@ type MvRadioPreset struct {
|
||||
}
|
||||
|
||||
type MvTopAdvertisersByIatum struct {
|
||||
Iata string `json:"iata"`
|
||||
NodeID uuid.UUID `json:"node_id"`
|
||||
Name *string `json:"name"`
|
||||
NodeType int16 `json:"node_type"`
|
||||
Bucket pgtype.Timestamptz `json:"bucket"`
|
||||
AdvertCount int64 `json:"advert_count"`
|
||||
LastHeard interface{} `json:"last_heard"`
|
||||
Iata string `json:"iata"`
|
||||
NodeID uuid.UUID `json:"node_id"`
|
||||
Name *string `json:"name"`
|
||||
NodeType int16 `json:"node_type"`
|
||||
Bucket pgtype.Timestamptz `json:"bucket"`
|
||||
AdvertCount int64 `json:"advert_count"`
|
||||
FloodAdvertCount int64 `json:"flood_advert_count"`
|
||||
DirectAdvertCount int64 `json:"direct_advert_count"`
|
||||
LastHeard interface{} `json:"last_heard"`
|
||||
}
|
||||
|
||||
type MvTopNodesByIatum struct {
|
||||
|
||||
+12
-6
@@ -1277,6 +1277,8 @@ SELECT
|
||||
name,
|
||||
node_type,
|
||||
COALESCE(SUM(advert_count), 0)::bigint AS advert_count,
|
||||
COALESCE(SUM(flood_advert_count), 0)::bigint AS flood_advert_count,
|
||||
COALESCE(SUM(direct_advert_count), 0)::bigint AS direct_advert_count,
|
||||
MAX(last_heard)::timestamptz AS last_heard,
|
||||
COALESCE(MAX(iata), '')::bpchar AS iata
|
||||
FROM mv_top_advertisers_by_iata
|
||||
@@ -1294,12 +1296,14 @@ type GetStatsTopAdvertisersParams struct {
|
||||
}
|
||||
|
||||
type GetStatsTopAdvertisersRow struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
Name *string `json:"name"`
|
||||
NodeType int16 `json:"node_type"`
|
||||
AdvertCount int64 `json:"advert_count"`
|
||||
LastHeard pgtype.Timestamptz `json:"last_heard"`
|
||||
Iata string `json:"iata"`
|
||||
ID uuid.UUID `json:"id"`
|
||||
Name *string `json:"name"`
|
||||
NodeType int16 `json:"node_type"`
|
||||
AdvertCount int64 `json:"advert_count"`
|
||||
FloodAdvertCount int64 `json:"flood_advert_count"`
|
||||
DirectAdvertCount int64 `json:"direct_advert_count"`
|
||||
LastHeard pgtype.Timestamptz `json:"last_heard"`
|
||||
Iata string `json:"iata"`
|
||||
}
|
||||
|
||||
// Top N advertisers in the window, summed from the hourly buckets.
|
||||
@@ -1317,6 +1321,8 @@ func (q *Queries) GetStatsTopAdvertisers(ctx context.Context, arg GetStatsTopAdv
|
||||
&i.Name,
|
||||
&i.NodeType,
|
||||
&i.AdvertCount,
|
||||
&i.FloodAdvertCount,
|
||||
&i.DirectAdvertCount,
|
||||
&i.LastHeard,
|
||||
&i.Iata,
|
||||
); err != nil {
|
||||
|
||||
+9
-7
@@ -146,13 +146,15 @@ func (s *Store) GetStatsTopAdvertisers(ctx context.Context, iatas []string, sinc
|
||||
items := make([]api.TopAdvertiser, 0, len(rows))
|
||||
for _, v := range rows {
|
||||
items = append(items, api.TopAdvertiser{
|
||||
NodeID: v.ID,
|
||||
NodeName: v.Name,
|
||||
NodeType: v.NodeType,
|
||||
NodeTypeName: api.NodeTypeName(v.NodeType),
|
||||
IATA: v.Iata,
|
||||
AdvertCount: v.AdvertCount,
|
||||
LastHeard: v.LastHeard.Time.UnixMilli(),
|
||||
NodeID: v.ID,
|
||||
NodeName: v.Name,
|
||||
NodeType: v.NodeType,
|
||||
NodeTypeName: api.NodeTypeName(v.NodeType),
|
||||
IATA: v.Iata,
|
||||
AdvertCount: v.AdvertCount,
|
||||
FloodAdvertCount: v.FloodAdvertCount,
|
||||
DirectAdvertCount: v.DirectAdvertCount,
|
||||
LastHeard: v.LastHeard.Time.UnixMilli(),
|
||||
})
|
||||
}
|
||||
return items, nil
|
||||
|
||||
@@ -114,6 +114,51 @@ func TestGetStatsTopObservers_IATATypeAssertion(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetStatsTopAdvertisers_FloodDirectSplit(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
mock := mockdb.NewMockQuerier(ctrl)
|
||||
|
||||
nodeID := uuid.MustParse("00000000-0000-0000-0000-000000000003")
|
||||
name := "test-node"
|
||||
heardAt := pgtype.Timestamptz{Time: time.Now(), Valid: true}
|
||||
|
||||
mock.EXPECT().
|
||||
GetStatsTopAdvertisers(gomock.Any(), gomock.Any()).
|
||||
Return([]sqlc.GetStatsTopAdvertisersRow{
|
||||
{
|
||||
ID: nodeID,
|
||||
Name: &name,
|
||||
NodeType: 2, // repeater
|
||||
AdvertCount: 10,
|
||||
FloodAdvertCount: 7,
|
||||
DirectAdvertCount: 3,
|
||||
LastHeard: heardAt,
|
||||
Iata: "YVR",
|
||||
},
|
||||
}, nil)
|
||||
|
||||
store := &Store{q: mock}
|
||||
items, err := store.GetStatsTopAdvertisers(context.Background(), []string{"YVR"}, time.Now().Add(-time.Hour), 5)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if len(items) != 1 {
|
||||
t.Fatalf("expected 1 item, got %d", len(items))
|
||||
}
|
||||
if items[0].AdvertCount != 10 {
|
||||
t.Errorf("expected AdvertCount 10, got %d", items[0].AdvertCount)
|
||||
}
|
||||
if items[0].FloodAdvertCount != 7 {
|
||||
t.Errorf("expected FloodAdvertCount 7, got %d", items[0].FloodAdvertCount)
|
||||
}
|
||||
if items[0].DirectAdvertCount != 3 {
|
||||
t.Errorf("expected DirectAdvertCount 3, got %d", items[0].DirectAdvertCount)
|
||||
}
|
||||
if items[0].FloodAdvertCount+items[0].DirectAdvertCount != items[0].AdvertCount {
|
||||
t.Error("expected FloodAdvertCount + DirectAdvertCount to equal AdvertCount")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetStatsNodeTypes_Mapping(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
mock := mockdb.NewMockQuerier(ctrl)
|
||||
|
||||
@@ -3559,6 +3559,13 @@ const docTemplate = `{
|
||||
"advertCount": {
|
||||
"type": "integer"
|
||||
},
|
||||
"directAdvertCount": {
|
||||
"type": "integer"
|
||||
},
|
||||
"floodAdvertCount": {
|
||||
"description": "FloodAdvertCount/DirectAdvertCount split AdvertCount by how the advert was routed:\nflood = route type 0 (transport_flood) or 1 (flood), broadcast with no known path;\ndirect = route type 2 (direct) or 3 (transport_direct), routed along a known path.\nFloodAdvertCount + DirectAdvertCount == AdvertCount.",
|
||||
"type": "integer"
|
||||
},
|
||||
"iata": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
@@ -3557,6 +3557,13 @@
|
||||
"advertCount": {
|
||||
"type": "integer"
|
||||
},
|
||||
"directAdvertCount": {
|
||||
"type": "integer"
|
||||
},
|
||||
"floodAdvertCount": {
|
||||
"description": "FloodAdvertCount/DirectAdvertCount split AdvertCount by how the advert was routed:\nflood = route type 0 (transport_flood) or 1 (flood), broadcast with no known path;\ndirect = route type 2 (direct) or 3 (transport_direct), routed along a known path.\nFloodAdvertCount + DirectAdvertCount == AdvertCount.",
|
||||
"type": "integer"
|
||||
},
|
||||
"iata": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
@@ -934,6 +934,15 @@ definitions:
|
||||
properties:
|
||||
advertCount:
|
||||
type: integer
|
||||
directAdvertCount:
|
||||
type: integer
|
||||
floodAdvertCount:
|
||||
description: |-
|
||||
FloodAdvertCount/DirectAdvertCount split AdvertCount by how the advert was routed:
|
||||
flood = route type 0 (transport_flood) or 1 (flood), broadcast with no known path;
|
||||
direct = route type 2 (direct) or 3 (transport_direct), routed along a known path.
|
||||
FloodAdvertCount + DirectAdvertCount == AdvertCount.
|
||||
type: integer
|
||||
iata:
|
||||
type: string
|
||||
lastHeard:
|
||||
|
||||
@@ -76,7 +76,13 @@ type TopAdvertiser struct {
|
||||
NodeTypeName string `json:"nodeTypeName"`
|
||||
IATA string `json:"iata"`
|
||||
AdvertCount int64 `json:"advertCount"`
|
||||
LastHeard int64 `json:"lastHeard"` // epoch ms
|
||||
// FloodAdvertCount/DirectAdvertCount split AdvertCount by how the advert was routed:
|
||||
// flood = route type 0 (transport_flood) or 1 (flood), broadcast with no known path;
|
||||
// direct = route type 2 (direct) or 3 (transport_direct), routed along a known path.
|
||||
// FloodAdvertCount + DirectAdvertCount == AdvertCount.
|
||||
FloodAdvertCount int64 `json:"floodAdvertCount"`
|
||||
DirectAdvertCount int64 `json:"directAdvertCount"`
|
||||
LastHeard int64 `json:"lastHeard"` // epoch ms
|
||||
}
|
||||
|
||||
// TopTalker is a companion name ranked by decrypted channel message count within the
|
||||
|
||||
Reference in New Issue
Block a user