// Copyright 2026 Beacon Contributors // SPDX-License-Identifier: AGPL-3.0-or-later // Package api defines the response types and read interface for the Beacon REST API. package api import ( "context" "encoding/json" "time" "github.com/google/uuid" ) // Page is a generic paginated response envelope used by all list endpoints // that support cursor-based pagination. NextCursor is the ID of the last item // returned and should be passed as the cursor param in the next request. // HasMore is true when additional results exist beyond the current page. type Page[T any] struct { Items []T `json:"items"` NextCursor *int64 `json:"nextCursor,omitempty"` HasMore bool `json:"hasMore"` } type Reader interface { // ListIATAs returns all known IATA codes with display name and coordinates. // IATAs are auto-created on first packet arrival from that location. ListIATAs(ctx context.Context) ([]IATA, error) // GetIATA returns a single IATA code by its 3-letter identifier. // Returns nil, error if the IATA code is not found. GetIATA(ctx context.Context, iata string) (*IATA, error) // GetIATABorder returns the raw GeoJSON Feature JSON for the given IATA's border. Returns // nil (no error) if the IATA exists but has no border configured -- the client renders // that as "nothing to draw", not an error. Returns an error if the IATA itself is unknown. GetIATABorder(ctx context.Context, iata string) (json.RawMessage, error) // ListRegions returns a summary list of all regions ordered by display_order then name. // Use GetRegion for full detail including associated IATAs. ListRegions(ctx context.Context) ([]RegionSummary, error) // GetRegion returns full detail for a single region including its associated IATA codes. // Returns nil, pgx.ErrNoRows if the region is not found. GetRegion(ctx context.Context, regionID int32) (*Region, error) // GetRegionBySlug returns full detail for a single region by its URL-safe slug. // Returns nil, pgx.ErrNoRows if the region is not found. GetRegionBySlug(ctx context.Context, slug string) (*Region, error) // ListChannels returns a paginated list of channels ordered by last seen. // Includes both hashtag-derived and explicit key channels. // Pass nil hash to skip hash filtering. Pass empty iatas to return all channels; // IATAs must be uppercase. // cursor is last_seen epoch ms of the last item; pass 0 to start from the beginning. ListChannels(ctx context.Context, limit int32, hash []byte, iatas []string, cursor int64) (Page[ChannelSummary], error) // GetChannel returns full detail for a single channel by its integer ID. // Returns nil, pgx.ErrNoRows if the channel is not found. GetChannel(ctx context.Context, channelID int32) (*Channel, error) // ListChannelMessages returns paginated messages for a channel identified by its integer ID. // Used by the /channels/{id}/messages endpoint. // Pass a zero time.Time for since to return all messages up to limit. // Pass empty string iata to return messages from all IATAs. // Pass cursor=0 to start from the beginning. ListChannelMessages(ctx context.Context, channelID *int32, since time.Time, limit int32, iatas []string, scope string, cursor int64) (Page[ChannelMessage], error) // ListChannelMessagesByHash returns paginated messages for all channels matching the given hash. // Used by the /messages?hash= endpoint. May return messages from multiple channels // if the hash collides across different keys. // Pass a zero time.Time for since to return all messages up to limit. // Pass empty string iata to return messages from all IATAs. // Pass cursor=0 to start from the beginning. ListChannelMessagesByHash(ctx context.Context, hash []byte, since time.Time, limit int32, iatas []string, scope string, cursor int64) (Page[ChannelMessage], error) // ListMessagesAfterID returns channel messages after the given message ID, // ordered oldest first. Used for WS reconnect backfill. ListMessagesAfterID(ctx context.Context, afterID int64, iatas []string, scope string, limit int32) ([]ChannelMessage, error) // ListObservers returns a paginated list of observers with optional filters. // All filter params are optional — pass empty string or nil to skip a filter. // status is "online" or "offline" derived from last_status_at recency. // cursor is last_seen epoch ms of the last observer; pass 0 to start from the beginning. ListObservers(ctx context.Context, iatas []string, observerType, broker, status, name, scope string, cursor int64, limit int32) (Page[ObserverSummary], error) // GetObserver returns full detail for a single observer by UUID. // Returns nil, pgx.ErrNoRows if the observer is not found. GetObserver(ctx context.Context, observerID uuid.UUID) (*Observer, error) // GetObserverTelemetry returns telemetry points for an observer within the given time range. // since and until define the window; pass zero times to use defaults (last 24h). GetObserverTelemetry(ctx context.Context, observerID uuid.UUID, since, until time.Time, afterID int64) (*ObserverTelemetry, error) // GetObserverTelemetryBucketed returns telemetry points for an observer bucketed into N-hour intervals. GetObserverTelemetryBucketed(ctx context.Context, observerID uuid.UUID, since, until time.Time, bucketHours int32) ([]ObserverTelemetryPoint, error) // GetObserverScopes returns the names of all transport scopes an observer has // been seen forwarding packets for, ordered alphabetically. GetObserverScopes(ctx context.Context, observerID uuid.UUID) ([]string, error) // ListObserverAdverts returns a paginated list of advert packets heard by an observer. // Pass cursor=0 to start from the beginning. ListObserverAdverts(ctx context.Context, observerID uuid.UUID, cursor int64, limit int32) (Page[AdvertObservation], error) // ListNodes returns a paginated list of nodes with optional filters. // When includeNeighbors is true, each NodeSummary's NeighborIDs field is // populated with the distinct set of neighbor node IDs (across all // IATAs); otherwise it's left nil to avoid the extra aggregation. ListNodes(ctx context.Context, nodeType int16, iatas []string, supportsMultibytePaths, supportsMultibyteTraces *bool, pubkey []byte, pubkeyPrefix, name, scope string, cursor int64, limit int32, includeNeighbors bool) (Page[NodeSummary], error) // GetNode returns full detail for a single node by UUID. // Returns nil, pgx.ErrNoRows if the node is not found. GetNode(ctx context.Context, nodeID uuid.UUID) (*Node, error) // GetNodesByIDs returns a map of node ID to resolved node details for the given IDs. GetNodesByIDs(ctx context.Context, ids []uuid.UUID) (map[uuid.UUID]*ResolvedNode, error) // ListNodeObservations returns a paginated list of packet observations originating from a node. // Pass cursor=0 to start from the beginning. ListNodeObservations(ctx context.Context, nodeID uuid.UUID, cursor int64, limit int32) (Page[PacketObservationSummary], error) // ListPackets returns a paginated list of packets with the latest observation rolled in. // Pass 0 for payloadType/routeType to skip those filters. // Pass nil for iatas, zero times for since/until to skip those filters. // Unfiltered lists order by global last_heard_at; when iatas are set, ordering // and the cursor are site-local (heard_at at the requested sites) instead. // cursor is epoch ms in either case; pass 0 to start from the beginning. // payloadTypes/routeTypes/scopes are OR'd within each filter, ANDed across filters; pass // nil/empty to skip a filter. ListPackets(ctx context.Context, payloadTypes, routeTypes []int16, iatas []string, scopes []string, since, until time.Time, cursor int64, limit int32) (Page[PacketSummary], error) // ListPacketsAfterID returns packets with observations after the given observation ID, // ordered oldest first. Used for WS reconnect backfill. ListPacketsAfterID(ctx context.Context, afterObservationID int64, payloadType, routeType int16, iatas []string, scope string, limit int32) ([]PacketSummary, error) // GetPacket returns full packet detail including all observations with radio settings. // Returns nil, pgx.ErrNoRows if not found. GetPacket(ctx context.Context, packetHash []byte) (*Packet, error) // GetRadioPresets returns radio preset usage grouped by preset and IATA(s). // Pass empty string for preset or nil iatas to skip those filters. GetRadioPresets(ctx context.Context, preset string, iatas []string) ([]RadioPreset, error) // GetStatsOverview returns top-line network figures for the last 24 hours. // Pass nil iatas to return stats across all IATAs. GetStatsOverview(ctx context.Context, iatas []string) (*StatsOverview, error) // GetStatsObservations returns hourly observation counts for charting. // Pass nil for iatas to return stats across all IATAs. // since defines the start of the window; pass zero time for default (last 7 days). GetStatsObservations(ctx context.Context, iatas []string, since time.Time) ([]ObservationPoint, error) // GetStatsPayloadBreakdown returns observation counts grouped by payload type. // Pass nil for iatas to return stats across all IATAs. // since defines the start of the window; pass zero time for default (last 24h). GetStatsPayloadBreakdown(ctx context.Context, iatas []string, since time.Time) ([]PayloadBreakdownItem, error) // GetStatsTopNodes returns the top N nodes by observation count. // Pass nil for iatas to return stats across all IATAs. GetStatsTopNodes(ctx context.Context, iatas []string, limit int32) ([]TopNode, error) // GetStatsTopObservers returns the top N observers by observation count. // Pass nil for iatas to return stats across all IATAs. // since defines the start of the window; pass zero time for default (last 24h). GetStatsTopObservers(ctx context.Context, iatas []string, since time.Time, limit int32) ([]TopObserver, error) // GetStatsTopAdvertisers returns the top N nodes by distinct ADVERT packet count. // Pass nil for iatas to return stats across all IATAs. // since defines the start of the window; pass zero time for default (last 24h). GetStatsTopAdvertisers(ctx context.Context, iatas []string, since time.Time, limit int32) ([]TopAdvertiser, error) // GetStatsClockDrift returns repeaters/room servers whose most recent advert-derived // clock drift exceeds the configured threshold (nodes.clock_drift_threshold), worst // drift first. Pass nil for iatas to return across all IATAs. Unlike the other stats // endpoints this isn't time-windowed -- it reflects each node's current drift state. GetStatsClockDrift(ctx context.Context, iatas []string, limit int32) ([]ClockDriftEntry, error) // GetStatsTopTalkers returns the top N companion names by decrypted channel message count. // Pass nil for iatas to return stats across all IATAs. // since defines the start of the window; pass zero time for default (last 24h). GetStatsTopTalkers(ctx context.Context, iatas []string, since time.Time, limit int32) ([]TopTalker, error) // GetScopeStats returns aggregate packet, observer and node counts per transport scope. GetScopeStats(ctx context.Context) ([]ScopeStats, error) // GetStatsNodeTypes returns node counts grouped by type, optionally filtered by IATA. GetStatsNodeTypes(ctx context.Context, iatas []string) ([]NodeTypeCount, error) // GetScopeNames returns the names of all configured transport scopes, ordered alphabetically. // Use when no geographic filter is applied — returns names only for a lightweight response. GetScopeNames(ctx context.Context) ([]string, error) // GetScopesByIATAs returns scope summaries filtered by the given IATA codes, // including observer, node and IATA counts. Expands region/regionId to IATAs automatically. GetScopesByIATAs(ctx context.Context, iatas []string) ([]ScopeSummary, error) // GetScopeByName returns full detail for a single scope by its normalized name (e.g. "#bc"), // including packet count, observer count, node count, and the list of IATAs it is active in. // Returns nil if the scope is not found. GetScopeByName(ctx context.Context, name string) (*ScopeDetail, error) // ListTraceTags returns a paginated list of trace tags with aggregate metadata. ListTraceTags(ctx context.Context, iatas []string, scope, traceType string, since, until time.Time, cursor time.Time, limit int32) ([]TraceTagSummary, error) // GetTraceByTag returns all packets for a given trace tag with resolved routes. GetTraceByTag(ctx context.Context, tag string) (*TraceDetail, error) // ListKnownRoutes returns known routes filtered by IATA and optional hop count. ListKnownRoutes(ctx context.Context, iata string, hopCount int32, cursor time.Time, limit int32) ([]KnownRoute, error) // SearchKnownRoutes returns known routes containing a path from source to destination hash. SearchKnownRoutes(ctx context.Context, iata, fromHash, toHash string) ([]KnownRoute, error) // GetNodeNeighbors returns the neighbors of a node ordered by most recently seen. GetNodeNeighbors(ctx context.Context, nodeID uuid.UUID) ([]NodeNeighbor, error) // GetKnownRoutesByNode returns all known routes in a given IATA that contain // the specified node UUID anywhere in their hop sequence. GetKnownRoutesByNode(ctx context.Context, iata string, nodeID uuid.UUID) ([]KnownRoute, error) // GetCrossIATANeighbors returns neighbors of a node that were observed in a // different IATA — indicating a potential cross-IATA radio link. GetCrossIATANeighbors(ctx context.Context, nodeID uuid.UUID, iata string) ([]NodeNeighbor, error) // SearchCrossIATARoutes finds routes that cross IATA boundaries between // a source node/IATA and a destination node/IATA. SearchCrossIATARoutes(ctx context.Context, fromHash, fromIATA, toHash, toIATA string) ([]CrossIATARoute, error) }