mirror of
https://github.com/MeshCore-Beacon/beacon-server.git
synced 2026-09-17 01:04:19 +00:00
update comments and readme after refactor
also typo bug on field in ListNodes and GetNode store functions
This commit is contained in:
@@ -44,25 +44,43 @@ For deployment instructions including the frontend app, see the deployment docs.
|
||||
|
||||
```
|
||||
tower-server/
|
||||
├── cmd/tower/ entry point
|
||||
├── cmd/tower/ entry point
|
||||
├── db/
|
||||
│ ├── migrations/ SQL schema (001_schema.sql)
|
||||
│ ├── queries/ sqlc query definitions
|
||||
│ ├── sqlc/ generated Go DB code (do not edit)
|
||||
│ └── store.go DB interface implementations
|
||||
│ ├── migrations/ SQL schema (001_schema.sql)
|
||||
│ ├── queries/ sqlc query definitions
|
||||
│ ├── sqlc/ generated Go DB code (do not edit)
|
||||
│ ├── store.go Store type, shared helpers
|
||||
│ ├── packets.go packet and observation store methods
|
||||
│ ├── nodes.go node store methods
|
||||
│ ├── observers.go observer store methods
|
||||
│ ├── channels.go channel and message store methods
|
||||
│ ├── stats.go stats and materialized view methods
|
||||
│ ├── config.go IATA and region store methods
|
||||
│ └── scopes.go transport scope store methods
|
||||
├── internal/
|
||||
│ ├── api/
|
||||
│ │ ├── handlers/ HTTP route handlers
|
||||
│ │ ├── middleware/ Auth middleware stub
|
||||
│ │ ├── router/ Chi router wiring
|
||||
│ │ ├── helpers.go Node type name helpers
|
||||
│ │ └── reader.go Read-only DB interface + response types
|
||||
│ ├── config/ Config file loading and DB seeding
|
||||
│ ├── hub/ WebSocket fan-out broker
|
||||
│ ├── ingest/ MQTT ingest pipeline
|
||||
│ ├── keystore/ Channel key store
|
||||
│ ├── scopestore/ Transport scope key store
|
||||
│ └── ws/ WebSocket handler
|
||||
│ │ ├── handlers/ HTTP route handlers
|
||||
│ │ ├── middleware/ Auth middleware stub
|
||||
│ │ ├── router/ Chi router wiring
|
||||
│ │ ├── reader.go Reader interface and Page type
|
||||
│ │ ├── packets.go packet response types and helpers
|
||||
│ │ ├── nodes.go node response types and helpers
|
||||
│ │ ├── observers.go observer response types
|
||||
│ │ ├── channels.go channel and message response types
|
||||
│ │ ├── stats.go stats response types
|
||||
│ │ ├── iata.go IATA response type
|
||||
│ │ └── regions.go region response types
|
||||
│ ├── config/ config file loading and DB seeding
|
||||
│ ├── hub/ WebSocket fan-out broker
|
||||
│ ├── ingest/
|
||||
│ │ ├── ingest.go Worker, DB interface, MQTT connection
|
||||
│ │ ├── packet.go packet pipeline, payload parsing
|
||||
│ │ ├── status.go status message handling
|
||||
│ │ ├── side_effects.go payload-type side effects (node upsert, channel messages)
|
||||
│ │ └── capability.go firmware capability detection
|
||||
│ ├── keystore/ channel key store
|
||||
│ ├── scopestore/ transport scope key store
|
||||
│ └── ws/ WebSocket handler and IP limiter
|
||||
├── config.yaml.example
|
||||
├── env.example
|
||||
├── docker-compose.yml
|
||||
|
||||
+2
-2
@@ -103,7 +103,7 @@ func (s *Store) ListNodes(ctx context.Context, nodeType int16, iatas []string, s
|
||||
Latitude: v.Latitude,
|
||||
Longitude: v.Longitude,
|
||||
IsObserver: v.IsObserver,
|
||||
ObvserverID: nullableUUID(v.ObserverID),
|
||||
ObserverID: nullableUUID(v.ObserverID),
|
||||
}
|
||||
if len(v.Iatas) > 0 {
|
||||
if err := json.Unmarshal(v.Iatas, &node.IATAs); err != nil {
|
||||
@@ -144,7 +144,7 @@ func (s *Store) GetNode(ctx context.Context, nodeID uuid.UUID) (*api.Node, error
|
||||
Latitude: row.Latitude,
|
||||
Longitude: row.Longitude,
|
||||
IsObserver: row.IsObserver,
|
||||
ObvserverID: nullableUUID(row.ObserverID),
|
||||
ObserverID: nullableUUID(row.ObserverID),
|
||||
DefaultScope: row.DefaultScopeName,
|
||||
},
|
||||
LocationSource: row.LocationSource,
|
||||
|
||||
@@ -15,14 +15,18 @@ import (
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
// Store wraps the sqlc-generated Queries and implements both ingest.DB and api.Reader.
|
||||
type Store struct {
|
||||
q *sqlc.Queries
|
||||
}
|
||||
|
||||
// New creates a Store backed by the given pgxpool connection pool.
|
||||
func New(pool *pgxpool.Pool) *Store {
|
||||
return &Store{q: sqlc.New(pool)}
|
||||
}
|
||||
|
||||
// ResolvePathHashes returns a map of hex-encoded path hash → matching node entries for
|
||||
// the given IATA. Hash size is inferred from the length of the first element in hashes.
|
||||
func (s *Store) ResolvePathHashes(ctx context.Context, iata string, hashes [][]byte) (map[string][]api.ResolvedPathEntry, error) {
|
||||
if len(hashes) == 0 {
|
||||
return nil, nil
|
||||
@@ -48,6 +52,7 @@ func (s *Store) ResolvePathHashes(ctx context.Context, iata string, hashes [][]b
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// nullableUUID returns nil for a zero UUID, or a pointer to the UUID otherwise.
|
||||
func nullableUUID(id uuid.UUID) *uuid.UUID {
|
||||
if id == (uuid.UUID{}) {
|
||||
return nil
|
||||
@@ -55,6 +60,8 @@ func nullableUUID(id uuid.UUID) *uuid.UUID {
|
||||
return &id
|
||||
}
|
||||
|
||||
// tristate converts a *bool to a SQL-friendly string for the ListNodes filter:
|
||||
// nil → "any", true → "true", false → "false".
|
||||
func tristate(b *bool) string {
|
||||
if b == nil {
|
||||
return "any"
|
||||
@@ -65,6 +72,7 @@ func tristate(b *bool) string {
|
||||
return "false"
|
||||
}
|
||||
|
||||
// toChannelMessage maps raw sqlc row fields to an api.ChannelMessage.
|
||||
func toChannelMessage(id int64, packetHashHex string, channelHash []byte, senderName *string, content *string, sentAt pgtype.Timestamptz, observationCount int64) api.ChannelMessage {
|
||||
sn := ""
|
||||
if senderName != nil {
|
||||
|
||||
@@ -8,18 +8,18 @@ type ChannelMessage struct {
|
||||
ChannelHash string `json:"channelHash"` // hex-encoded single-byte channel hash
|
||||
SenderName string `json:"senderName"` // display name from the decrypted payload
|
||||
Content string `json:"content"` // decrypted message text
|
||||
SentAt int64 `json:"sentAt"` // epoch ms
|
||||
ObservationCount int64 `json:"observationCount"` // the number of observations for this message packet hash
|
||||
SentAt int64 `json:"sentAt"` // epoch ms, from the sender's embedded timestamp
|
||||
ObservationCount int64 `json:"observationCount"` // number of packet_observations rows for this message's packet hash
|
||||
}
|
||||
|
||||
// ChannelSummary is the minimal channel representation used in list responses.
|
||||
type ChannelSummary struct {
|
||||
ID int `json:"id"`
|
||||
Name *string `json:"name,omitempty"` // display name, nil if not set
|
||||
Name *string `json:"name,omitempty"` // display name from config or nil
|
||||
ChannelHash string `json:"channelHash"` // hex-encoded single-byte hash
|
||||
LastSeen int64 `json:"lastSeen"` // epoch ms
|
||||
IsHashtag bool `json:"isHashtag"` // true if derived from a hashtag PSK
|
||||
KeyKnown bool `json:"keyKnown"` // true if Tower has a decryption key
|
||||
LastSeen int64 `json:"lastSeen"` // epoch ms, time of most recent message
|
||||
IsHashtag bool `json:"isHashtag"` // true if key was derived from a hashtag PSK
|
||||
KeyKnown bool `json:"keyKnown"` // true if Tower has a decryption key for this channel
|
||||
}
|
||||
|
||||
// Channel is the full channel representation including decryption metadata.
|
||||
@@ -27,7 +27,7 @@ type ChannelSummary struct {
|
||||
// publicly derivable from the tag name.
|
||||
type Channel struct {
|
||||
ChannelSummary
|
||||
Hashtag *string `json:"hashtag,omitempty"` // tag name without # prefix
|
||||
Hashtag *string `json:"hashtag,omitempty"` // tag name without # prefix; non-nil only for hashtag channels
|
||||
KeyFingerprint *string `json:"keyFingerprint,omitempty"` // first 8 bytes of SHA256(key), hex-encoded
|
||||
MessageCount int64 `json:"messageCount"`
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package api
|
||||
|
||||
// IATA represents a known airport/location code used to group observers and packets.
|
||||
// IATAs are auto-created on first packet arrival from that location.
|
||||
// DisplayName, Lat and Lng are optional — they are set via config file override
|
||||
// or remain nil if the IATA was auto-created from packet traffic.
|
||||
type IATA struct {
|
||||
|
||||
+12
-12
@@ -16,27 +16,27 @@ type NodeIATA struct {
|
||||
// NodeSummary is the minimal node representation used in list responses.
|
||||
type NodeSummary struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
PublicKey string `json:"publicKey"` // hex-encoded public key
|
||||
NodeType int16 `json:"nodeType"` // 1=companion, 2=repeater, 3=room server
|
||||
PublicKey string `json:"publicKey"` // hex-encoded Ed25519 public key
|
||||
NodeType int16 `json:"nodeType"` // 1=companion, 2=repeater, 3=room_server, 4=sensor
|
||||
NodeTypeName string `json:"nodeTypeName"`
|
||||
Name *string `json:"name,omitempty"`
|
||||
IsObserver bool `json:"isObserver"`
|
||||
ObvserverID *uuid.UUID `json:"observerId,omitempty"`
|
||||
Latitude *float64 `json:"lat,omitempty"`
|
||||
Longitude *float64 `json:"lng,omitempty"`
|
||||
Radio *string `json:"radio,omitempty"`
|
||||
IATAs []NodeIATA `json:"iatas"`
|
||||
DefaultScope *string `json:"defaultScope,omitempty"`
|
||||
IsObserver bool `json:"isObserver"` // true if this node is also a known observer
|
||||
ObserverID *uuid.UUID `json:"observerId,omitempty"` // UUID of the associated observer row, if any
|
||||
Latitude *float64 `json:"lat,omitempty"` // decimal degrees, from advert AppData
|
||||
Longitude *float64 `json:"lng,omitempty"` // decimal degrees, from advert AppData
|
||||
Radio *string `json:"radio,omitempty"` // shorthand: "freqMhz,bwKhz,sf" e.g. "910.5,62.5,7"
|
||||
IATAs []NodeIATA `json:"iatas"` // IATAs where this node has been heard, with last heard timestamps
|
||||
DefaultScope *string `json:"defaultScope,omitempty"` // most recently matched transport scope name e.g. "#bc"
|
||||
}
|
||||
|
||||
// Node is the full node representation including firmware capability flags,
|
||||
// location source, and timing metadata.
|
||||
type Node struct {
|
||||
NodeSummary
|
||||
LocationSource *string `json:"locationSource,omitempty"` // e.g. "advert", "manual"
|
||||
LocationSource *string `json:"locationSource,omitempty"` // "advert" or "manual"
|
||||
LastAdvertAt *int64 `json:"lastAdvertAt,omitempty"` // epoch ms, nil if no advert received
|
||||
SupportsMultibytePaths bool `json:"supportsMultibytePaths"` // firmware >= 1.14.0
|
||||
SupportsMultibyteTraces bool `json:"supportsMultibyteTraces"` // firmware >= 1.11.0
|
||||
SupportsMultibytePaths bool `json:"supportsMultibytePaths"` // firmware >= 1.14.0; detected via path hash size
|
||||
SupportsMultibyteTraces bool `json:"supportsMultibyteTraces"` // firmware >= 1.11.0; detected via trace hash size
|
||||
MinFirmwareVersion *string `json:"minFirmwareVersion,omitempty"` // derived from capability flags
|
||||
FirstSeen int64 `json:"firstSeen"` // epoch ms
|
||||
LastSeen int64 `json:"lastSeen"` // epoch ms
|
||||
|
||||
+41
-31
@@ -23,18 +23,20 @@ type PacketSummary struct {
|
||||
PayloadTypeName string `json:"payloadTypeName"`
|
||||
RouteType int16 `json:"routeType"`
|
||||
RouteTypeName string `json:"routeTypeName"`
|
||||
Scope *string `json:"scope,omitempty"`
|
||||
FirstHeardAt int64 `json:"firstHeardAt"` // epoch ms
|
||||
LastHeardAt int64 `json:"lastHeardAt"` // epoch ms
|
||||
Scope *string `json:"scope,omitempty"` // matched transport scope name e.g. "#bc"
|
||||
FirstHeardAt int64 `json:"firstHeardAt"` // epoch ms
|
||||
LastHeardAt int64 `json:"lastHeardAt"` // epoch ms
|
||||
ObservationCount int32 `json:"observationCount"`
|
||||
LatestObserver *PacketLatestObserver `json:"latestObserver,omitempty"`
|
||||
Summary *string `json:"summary,omitempty"` // human-readable payload summary
|
||||
}
|
||||
|
||||
// PacketPathLength is the decoded path_length byte from a packet observation.
|
||||
// The raw byte encodes both hash size and hop count in a bit-packed format (§2.5).
|
||||
type PacketPathLength struct {
|
||||
Raw string `json:"raw"`
|
||||
HashSize int16 `json:"hashSize"`
|
||||
HopCount int16 `json:"hopCount"`
|
||||
Raw string `json:"raw"` // hex-encoded single byte
|
||||
HashSize int16 `json:"hashSize"` // per-hop hash size in bytes (1, 2, or 3)
|
||||
HopCount int16 `json:"hopCount"` // number of path hashes present
|
||||
}
|
||||
|
||||
// PacketObservationDetail is a full observation including radio settings and resolved path.
|
||||
@@ -45,16 +47,16 @@ type PacketObservationDetail struct {
|
||||
IATA string `json:"iata"`
|
||||
HeardAt int64 `json:"heardAt"` // epoch ms
|
||||
PathLength PacketPathLength `json:"pathLength"`
|
||||
PathBytes *string `json:"pathBytes,omitempty"` // hex-encoded
|
||||
PathBytes *string `json:"pathBytes,omitempty"` // hex-encoded accumulated path hashes
|
||||
RSSI *int16 `json:"rssi,omitempty"`
|
||||
SNR *float32 `json:"snr,omitempty"`
|
||||
PropagationTimeMs *int32 `json:"propagationTimeMs"`
|
||||
PropagationTimeMs *int32 `json:"propagationTimeMs"` // ms since first observation; 0 for first
|
||||
Radio *PacketRadio `json:"radio,omitempty"`
|
||||
SourceBroker string `json:"sourceBroker"`
|
||||
ResolvedPath []ResolvedHop `json:"resolvedPath"`
|
||||
ResolvedPath []ResolvedHop `json:"resolvedPath"` // per-observation resolved path hashes
|
||||
}
|
||||
|
||||
// PacketRadio holds the radio settings from the observation.
|
||||
// PacketRadio holds the radio settings copied from the observer at observation time.
|
||||
type PacketRadio struct {
|
||||
FreqMHz *float32 `json:"freqMhz,omitempty"`
|
||||
SpreadFactor *int16 `json:"spreadFactor,omitempty"`
|
||||
@@ -63,20 +65,23 @@ type PacketRadio struct {
|
||||
}
|
||||
|
||||
// ResolvedHop is a single hop in a packet's resolved path.
|
||||
// Confidence is "high" (exactly one match), "ambiguous" (multiple matches), or "none" (no match).
|
||||
type ResolvedHop struct {
|
||||
Confidence string `json:"confidence"` // "high", "low", "unknown"
|
||||
Nodes []ResolvedNode `json:"nodes"`
|
||||
Confidence string `json:"confidence"` // "high", "ambiguous", or "none"
|
||||
Nodes []ResolvedNode `json:"nodes"` // empty for "none", one for "high", multiple for "ambiguous"
|
||||
}
|
||||
|
||||
// ResolvedNode is a node reference within a resolved path hop.
|
||||
type ResolvedNode struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
Name *string `json:"name,omitempty"`
|
||||
PublicKey string `json:"publicKey"` // hex-encoded prefix
|
||||
PublicKey string `json:"publicKey"` // hex-encoded prefix used for resolution
|
||||
Latitude *float64 `json:"latitude,omitempty"`
|
||||
Longitude *float64 `json:"longitude,omitempty"`
|
||||
}
|
||||
|
||||
// ResolvedPathEntry is an internal type used by the store layer to carry node
|
||||
// details returned from ResolvePathHashes before mapping to ResolvedNode.
|
||||
type ResolvedPathEntry struct {
|
||||
NodeID uuid.UUID
|
||||
Name *string
|
||||
@@ -85,15 +90,20 @@ type ResolvedPathEntry struct {
|
||||
PublicKey []byte
|
||||
}
|
||||
|
||||
// PacketHeader holds the decoded header byte and its bit-packed fields.
|
||||
// The raw header byte encodes payload version, payload type, and route type (§2.3).
|
||||
type PacketHeader struct {
|
||||
Raw string `json:"raw"`
|
||||
RouteType int16 `json:"routeType"`
|
||||
RouteTypeName string `json:"routeTypeName"`
|
||||
PayloadType int16 `json:"payloadType"`
|
||||
PayloadTypeName string `json:"payloadTypeName"`
|
||||
PayloadVersion int16 `json:"payloadVersion"`
|
||||
Raw string `json:"raw"` // hex-encoded single byte
|
||||
RouteType int16 `json:"routeType"` // bits 0-1
|
||||
RouteTypeName string `json:"routeTypeName"` // FLOOD, DIRECT, TRANSPORT_FLOOD, TRANSPORT_DIRECT
|
||||
PayloadType int16 `json:"payloadType"` // bits 2-5
|
||||
PayloadTypeName string `json:"payloadTypeName"` // advert, request, group_text, etc.
|
||||
PayloadVersion int16 `json:"payloadVersion"` // bits 6-7
|
||||
}
|
||||
|
||||
// PacketTransportCodes holds the decoded transport codes present in TRANSPORT_FLOOD
|
||||
// and TRANSPORT_DIRECT packets. RegionCode is transport_code_1; SubRegionCode is
|
||||
// transport_code_2 (reserved in v1, always 0 on the wire).
|
||||
type PacketTransportCodes struct {
|
||||
RegionCode int32 `json:"regionCode"`
|
||||
SubRegionCode int32 `json:"subRegionCode"`
|
||||
@@ -104,17 +114,17 @@ type Packet struct {
|
||||
PacketHash string `json:"packetHash"`
|
||||
Header PacketHeader `json:"header"`
|
||||
TransportCodes *PacketTransportCodes `json:"transportCodes,omitempty"`
|
||||
OriginPubkey *string `json:"originPubkey,omitempty"`
|
||||
OriginPubkey *string `json:"originPubkey,omitempty"` // hex-encoded; nil when not extractable from payload
|
||||
ParsedPayload json.RawMessage `json:"parsedPayload,omitempty"`
|
||||
RawPayload string `json:"rawPayload"`
|
||||
Decrypted bool `json:"decrypted"`
|
||||
ChannelHash *string `json:"channelHash,omitempty"`
|
||||
Scope *string `json:"scope,omitempty"`
|
||||
FirstHeardAt int64 `json:"firstHeardAt"`
|
||||
LastHeardAt int64 `json:"lastHeardAt"`
|
||||
FirstToLastMs int64 `json:"firstToLastMs"`
|
||||
RawPayload string `json:"rawPayload"` // hex-encoded payload bytes (excludes header and path)
|
||||
Decrypted bool `json:"decrypted"` // true if group text was successfully decrypted
|
||||
ChannelHash *string `json:"channelHash,omitempty"` // hex-encoded single byte; non-nil for group_text/group_data
|
||||
Scope *string `json:"scope,omitempty"` // matched transport scope name e.g. "#bc"
|
||||
FirstHeardAt int64 `json:"firstHeardAt"` // epoch ms
|
||||
LastHeardAt int64 `json:"lastHeardAt"` // epoch ms
|
||||
FirstToLastMs int64 `json:"firstToLastMs"` // ms between first and last observation
|
||||
ObservationCount int32 `json:"observationCount"`
|
||||
ResolvedRoute []ResolvedHop `json:"resolvedRoute,omitempty"`
|
||||
ResolvedRoute []ResolvedHop `json:"resolvedRoute,omitempty"` // trace packets only: resolved intended route
|
||||
Observations []PacketObservationDetail `json:"observations"`
|
||||
}
|
||||
|
||||
@@ -123,14 +133,14 @@ type Packet struct {
|
||||
type AdvertObservation struct {
|
||||
PacketObservationSummary
|
||||
NodeName *string `json:"nodeName,omitempty"`
|
||||
NodePublicKey *string `json:"nodePublicKey,omitempty"`
|
||||
NodePublicKey *string `json:"nodePublicKey,omitempty"` // hex-encoded
|
||||
}
|
||||
|
||||
// PacketObservationSummary is a lightweight packet+observation pair used in
|
||||
// list contexts such as observer adverts and node observations.
|
||||
type PacketObservationSummary struct {
|
||||
ID int64 `json:"id"` // observation ID, use as cursor for pagination
|
||||
PacketHash string `json:"packetHash"` // hex-encoded
|
||||
ID int64 `json:"id"` // observation ID, use as cursor for pagination
|
||||
PacketHash string `json:"packetHash"` // hex-encoded
|
||||
PayloadType int16 `json:"payloadType"`
|
||||
PayloadTypeName string `json:"payloadTypeName"`
|
||||
IATA string `json:"iata"`
|
||||
|
||||
+11
-10
@@ -2,12 +2,13 @@ package api
|
||||
|
||||
import "github.com/google/uuid"
|
||||
|
||||
// RadioPreset represents a unique radio configuration and where it is heard.
|
||||
// RadioPreset represents a unique radio configuration observed in a given IATA,
|
||||
// aggregated from both observer status messages and node adverts.
|
||||
type RadioPreset struct {
|
||||
Preset string `json:"preset"`
|
||||
Preset string `json:"preset"` // "freqMhz,bwKhz,sf" e.g. "910.525,62.5,7"
|
||||
IATA string `json:"iata"`
|
||||
SourceType string `json:"sourceType"` // "observer" or "node"
|
||||
Count int64 `json:"count"`
|
||||
Count int64 `json:"count"` // number of observers or nodes on this preset in this IATA
|
||||
}
|
||||
|
||||
// StatsOverview is the top-level network summary for the overview endpoint.
|
||||
@@ -19,9 +20,9 @@ type StatsOverview struct {
|
||||
WindowHours int `json:"windowHours"` // always 24 for now
|
||||
}
|
||||
|
||||
// ObservationPoint is a single time-bucketed observation count.
|
||||
// ObservationPoint is a single time-bucketed observation count for charting.
|
||||
type ObservationPoint struct {
|
||||
Hour int64 `json:"hour"` // epoch ms, start of bucket
|
||||
Hour int64 `json:"hour"` // epoch ms, start of the 1-hour bucket
|
||||
IATA string `json:"iata"`
|
||||
ObservationCount int64 `json:"observationCount"`
|
||||
UniquePackets int64 `json:"uniquePackets"`
|
||||
@@ -37,13 +38,13 @@ type PayloadBreakdownItem struct {
|
||||
|
||||
// ScopeStats represents aggregate statistics for a single transport scope.
|
||||
type ScopeStats struct {
|
||||
Name string `json:"name"`
|
||||
PacketCount int64 `json:"packetCount"`
|
||||
ObserverCount int64 `json:"observerCount"`
|
||||
NodeCount int64 `json:"nodeCount"`
|
||||
Name string `json:"name"` // normalized scope name e.g. "#bc"
|
||||
PacketCount int64 `json:"packetCount"` // distinct packets matched to this scope
|
||||
ObserverCount int64 `json:"observerCount"` // distinct observers that forwarded packets in this scope
|
||||
NodeCount int64 `json:"nodeCount"` // distinct nodes with this as their default scope
|
||||
}
|
||||
|
||||
// TopNode is a node ranked by observation count.
|
||||
// TopNode is a node ranked by observation count from the mv_top_nodes_by_iata materialized view.
|
||||
type TopNode struct {
|
||||
NodeID uuid.UUID `json:"nodeId"`
|
||||
NodeName *string `json:"nodeName,omitempty"`
|
||||
|
||||
Reference in New Issue
Block a user