update comments and readme after refactor

also typo bug on field in ListNodes and GetNode store functions
This commit is contained in:
Enot (ded) Skelly
2026-06-04 14:38:25 -07:00
parent 4425e3ffa2
commit 7b79db1f3f
8 changed files with 116 additions and 78 deletions
+34 -16
View File
@@ -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
View File
@@ -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,
+8
View File
@@ -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 {
+7 -7
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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"`