From 7b79db1f3f391aa57717e03af77caf9e87a1fe8b Mon Sep 17 00:00:00 2001 From: "Enot (ded) Skelly" Date: Thu, 4 Jun 2026 14:38:25 -0700 Subject: [PATCH] update comments and readme after refactor also typo bug on field in ListNodes and GetNode store functions --- README.md | 50 +++++++++++++++++++--------- db/nodes.go | 4 +-- db/store.go | 8 +++++ internal/api/channels.go | 14 ++++---- internal/api/iata.go | 1 + internal/api/nodes.go | 24 +++++++------- internal/api/packets.go | 72 +++++++++++++++++++++++----------------- internal/api/stats.go | 21 ++++++------ 8 files changed, 116 insertions(+), 78 deletions(-) diff --git a/README.md b/README.md index 1ce8ba9..c86ab80 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/db/nodes.go b/db/nodes.go index 50884e4..ffc70f4 100644 --- a/db/nodes.go +++ b/db/nodes.go @@ -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, diff --git a/db/store.go b/db/store.go index 488a099..f4dcc98 100644 --- a/db/store.go +++ b/db/store.go @@ -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 { diff --git a/internal/api/channels.go b/internal/api/channels.go index 49cc031..5630d7c 100644 --- a/internal/api/channels.go +++ b/internal/api/channels.go @@ -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"` } diff --git a/internal/api/iata.go b/internal/api/iata.go index e351266..1dfc9e4 100644 --- a/internal/api/iata.go +++ b/internal/api/iata.go @@ -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 { diff --git a/internal/api/nodes.go b/internal/api/nodes.go index c1df084..b054c8c 100644 --- a/internal/api/nodes.go +++ b/internal/api/nodes.go @@ -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 diff --git a/internal/api/packets.go b/internal/api/packets.go index da7cb2d..773a595 100644 --- a/internal/api/packets.go +++ b/internal/api/packets.go @@ -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"` diff --git a/internal/api/stats.go b/internal/api/stats.go index 085fa24..8d9b076 100644 --- a/internal/api/stats.go +++ b/internal/api/stats.go @@ -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"`