Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -496,6 +496,21 @@ jobs:
CHROMIUM_REQUIRE=1 CORESCOPE_SERVER_BIN=./corescope-server CORESCOPE_INGESTOR_BIN=./corescope-ingestor FIXTURE_DB=test-fixtures/e2e-fixture.db node test-channel-proposals-e2e.js 2>&1 | tee -a e2e-output.txt
# Starts its own ingestor + server on a temp copy of the fixture; seeds client-decryptable packets itself.
CHROMIUM_REQUIRE=1 CORESCOPE_SERVER_BIN=./corescope-server CORESCOPE_INGESTOR_BIN=./corescope-ingestor FIXTURE_DB=test-fixtures/e2e-fixture.db node test-channels-client-state-152-decrypt-e2e.js 2>&1 | tee -a e2e-output.txt
# #315 uses synthetic data and its own default/on/off Go servers.
# Pin the existing Leaflet runtime; browser network stays local-only.
leaflet_dir="$RUNNER_TEMP/corescope-315-leaflet"
mkdir -p "$leaflet_dir/images"
curl --fail --silent --show-error --location --retry 3 https://unpkg.com/leaflet@1.9.4/dist/leaflet.js -o "$leaflet_dir/leaflet.js"
curl --fail --silent --show-error --location --retry 3 https://unpkg.com/leaflet@1.9.4/dist/leaflet.css -o "$leaflet_dir/leaflet.css"
curl --fail --silent --show-error --location --retry 3 https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon.png -o "$leaflet_dir/images/marker-icon.png"
curl --fail --silent --show-error --location --retry 3 https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon-2x.png -o "$leaflet_dir/images/marker-icon-2x.png"
curl --fail --silent --show-error --location --retry 3 https://unpkg.com/leaflet@1.9.4/dist/images/marker-shadow.png -o "$leaflet_dir/images/marker-shadow.png"
echo "db49d009c841f5ca34a888c96511ae936fd9f5533e90d8b2c4d57596f4e5641a $leaflet_dir/leaflet.js" | sha256sum --check
echo "a7837102824184820dfa198d1ebcd109ff6d0ff9a2672a074b9a1b4d147d04c6 $leaflet_dir/leaflet.css" | sha256sum --check
echo "574c3a5cca85f4114085b6841596d62f00d7c892c7b03f28cbfa301deb1dc437 $leaflet_dir/images/marker-icon.png" | sha256sum --check
echo "00179c4c1ee830d3a108412ae0d294f55776cfeb085c60129a39aa6fc4ae2528 $leaflet_dir/images/marker-icon-2x.png" | sha256sum --check
echo "264f5c640339f042dd729062cfc04c17f8ea0f29882b538e3848ed8f10edb4da $leaflet_dir/images/marker-shadow.png" | sha256sum --check
CHROMIUM_REQUIRE=1 CORESCOPE_SERVER_BIN=./corescope-server CORESCOPE_MIGRATE_BIN=./corescope-migrate CORESCOPE_LEAFLET_DIR="$leaflet_dir" node test-issue-315-estimated-positions-e2e.js 2>&1 | tee -a e2e-output.txt
BASE_URL=http://localhost:13581 node test-issue-1236-map-mobile-e2e.js 2>&1 | tee -a e2e-output.txt
BASE_URL=http://localhost:13581 node test-issue-1329-map-controls-accordion-e2e.js 2>&1 | tee -a e2e-output.txt
BASE_URL=http://localhost:13581 node test-issue-1273-qr-overlay-height-e2e.js 2>&1 | tee -a e2e-output.txt
Expand Down
7 changes: 7 additions & 0 deletions cmd/server/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,10 @@ type Config struct {
DBPath string `json:"dbPath"`
ListLimits *ListLimitsConfig `json:"listLimits"`

// EstimatedPositions is an operator-side startup policy. Missing means
// enabled for compatibility; changes require a server restart.
EstimatedPositions *EstimatedPositionsConfig `json:"estimatedPositions,omitempty"`

// ChannelProposals configures publicly suggested hashtag channels
// (internal/channelregistry). Submissions open only when enabled AND a
// strong apiKey is set; the same block is read by the ingestor.
Expand Down Expand Up @@ -613,6 +617,9 @@ func LoadConfig(baseDirs ...string) (*Config, error) {
if err != nil {
continue
}
if err := validateEstimatedPositionsConfig(data); err != nil {
return nil, fmt.Errorf("config %s: %w", p, err)
}
if err := json.Unmarshal(data, cfg); err != nil {
continue
}
Expand Down
53 changes: 30 additions & 23 deletions cmd/server/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -2168,7 +2168,7 @@ type PacketPathPoint struct {
}

// PacketPathObserver is the station that produced a given branch's
// observation of a packet (see GetPacketPath), positioned from its own
// observation of a packet (see getPacketPath), positioned from its own
// self-advertised GPS (the same source /api/observers uses) when known,
// falling back to its configured IATA code, and finally its strongest
// neighbor_edges neighbor's position (Approx=true), otherwise -- not a
Expand Down Expand Up @@ -2276,7 +2276,7 @@ type PacketPathResponse struct {
TxID int64 `json:"-"`
}

// GetPacketPath resolves every distinct station that observed a packet to
// getPacketPath resolves every distinct station that observed a packet to
// its own branch: hop count and (where resolvable) relay names/positions
// in path order, plus that station's own position. A station can hear a
// packet more than once as flood copies arrive via different routes; only
Expand All @@ -2290,8 +2290,8 @@ type PacketPathResponse struct {
// geo-sanity filter for the Approx position fallback (see its doc
// comment) -- pass Config.NeighborMaxEdgeKm().
// obsBranch is one candidate branch of a packet's path: the deepest-hop
// observation attributed to a single observer. Shared by GetPacketPath
// (built from one hash's rows) and GetPacketPathsBulk (built the same way,
// observation attributed to a single observer. Shared by getPacketPath
// (built from one hash's rows) and getPacketPathsBulk (built the same way,
// per hash, from a multi-hash result set) via parsePacketPathObsRow so the
// two can never parse a row differently.
type obsBranch struct {
Expand All @@ -2317,8 +2317,8 @@ type packetPathNodeInfo struct {

// packetPathReduction accumulates one hash's observation rows into the
// deepest-hop branch per observer (best) and the single earliest-arriving
// branch overall (first), exactly as GetPacketPath's original inline loop
// did. GetPacketPathsBulk keeps one packetPathReduction per hash while
// branch overall (first), exactly as getPacketPath's original inline loop
// did. getPacketPathsBulk keeps one packetPathReduction per hash while
// scanning a combined multi-hash result set.
type packetPathReduction struct {
best map[string]*obsBranch
Expand All @@ -2336,8 +2336,8 @@ func newPacketPathReduction() *packetPathReduction {
// (observations.id), and "earliest wins" ties on equal timestamp the same
// way. obsID is a real, stable, monotonically-assigned DB identity (unlike
// scan order, which the query planner is free to vary between the
// single-hash query GetPacketPath issues and the multi-hash query
// GetPacketPathsBulk issues) -- so both paths pick the identical branch on
// single-hash query getPacketPath issues and the multi-hash query
// getPacketPathsBulk issues) -- so both paths pick the identical branch on
// a tie regardless of any difference in how their rows happen to arrive.
// This determinizes previously-undefined behavior; it does not preserve
// any order that was ever guaranteed before.
Expand All @@ -2355,7 +2355,7 @@ func (r *packetPathReduction) fold(key string, branch *obsBranch, tsValid bool,
}

// parsePacketPathObsRow parses one row of the packet-path observations/
// transmissions join (GetPacketPath and GetPacketPathsBulk use the same
// transmissions join (getPacketPath and getPacketPathsBulk use the same
// column order, bulk with one leading `hash` column and both with a
// trailing `o.id` column) into an obsBranch and its best-map key. ok is
// false for rows that can't contribute a branch -- missing/unparsable
Expand Down Expand Up @@ -2522,15 +2522,15 @@ func dedupPacketPathStrings(ss []string) []string {
// (0,0) sentinel position the same way GetNodesForScopeAdoption and
// geofilter.PassesFilter do. Input is deduped and chunked at
// packetPathNodeLookupChunkSize bind parameters per query -- the caller may
// pass an arbitrarily large pubkey set (e.g. GetPacketPathsBulk's whole-batch
// pass an arbitrarily large pubkey set (e.g. getPacketPathsBulk's whole-batch
// union). If any chunk's query or scan fails, the entire call fails --
// (nil, error), never a partial map, even though earlier chunks may have
// already resolved cleanly; there is no cross-chunk aggregation logic
// needed beyond that abort, since each pubkey is confined to exactly one
// chunk (dedup happens before chunking) and therefore writes exactly one
// map entry regardless of chunk order. Shared by GetPacketPath (which
// map entry regardless of chunk order. Shared by getPacketPath (which
// discards the error, preserving its existing tolerant-on-query-failure
// behavior unchanged) and GetPacketPathsBulk (which propagates it, per the
// behavior unchanged) and getPacketPathsBulk (which propagates it, per the
// bulk helpers' explicit-error contract).
func (db *DB) resolveNodesByPubkey(pubkeys []string) (map[string]packetPathNodeInfo, error) {
nodeByPK := make(map[string]packetPathNodeInfo, len(pubkeys))
Expand Down Expand Up @@ -2594,8 +2594,8 @@ func (db *DB) resolveNodesByPubkey(pubkeys []string) (map[string]packetPathNodeI
// unique name to exactly one chunk, but the logic doesn't rely on that) is
// still detected correctly rather than only within its own chunk. Any
// chunk's query/scan failure fails the entire call -- (nil, error), never a
// partial map. Shared by GetPacketPath (discards the error, preserving
// existing behavior) and GetPacketPathsBulk (propagates it).
// partial map. Shared by getPacketPath (discards the error, preserving
// existing behavior) and getPacketPathsBulk (propagates it).
func (db *DB) resolveNodesByName(names []string) (map[string]packetPathNodeInfo, error) {
nodeByName := make(map[string]packetPathNodeInfo, len(names))
if len(names) == 0 {
Expand Down Expand Up @@ -2666,10 +2666,10 @@ type neighborEstimate struct {
// hash, given already-resolved node position maps and a neighbor-estimate
// lookup. This is the single shared implementation of branch assembly,
// hop-point/observer position resolution, DistanceFromFirstKm, and sort
// order -- used identically by GetPacketPath (single hash, maps resolved
// order -- used identically by getPacketPath (single hash, maps resolved
// via a per-hash query, neighborLookup calling nearestPositionedNeighbor
// directly on demand, unchanged from before this refactor) and
// GetPacketPathsBulk (many hashes, maps resolved via one batched query
// getPacketPathsBulk (many hashes, maps resolved via one batched query
// across the whole request, neighborLookup reading a pre-fetched map so no
// per-point query happens here). Changing this function changes both paths
// identically -- they cannot silently diverge.
Expand Down Expand Up @@ -2815,7 +2815,11 @@ func buildPacketPathResponseFromReduction(
return resp
}

func (db *DB) GetPacketPath(hash string, maxEdgeKm float64) (*PacketPathResponse, error) {
// getPacketPath takes the caller's immutable operator policy explicitly;
// shared DB handles never hold mutable instance configuration. There is
// deliberately no always-estimating exported wrapper: a caller that did
// not state a policy would bypass the operator's #315 setting.
func (db *DB) getPacketPath(hash string, maxEdgeKm float64, estimatesEnabled bool) (*PacketPathResponse, error) {
if !db.hasResolvedPath() {
return nil, fmt.Errorf("resolved_path not available on this server")
}
Expand Down Expand Up @@ -2870,9 +2874,9 @@ func (db *DB) GetPacketPath(hash string, maxEdgeKm float64) (*PacketPathResponse
for pk := range pubkeySet {
pubkeys = append(pubkeys, pk)
}
// Error discarded here on purpose -- preserves GetPacketPath's existing
// Error discarded here on purpose -- preserves getPacketPath's existing
// tolerant-on-query-failure behavior (a failed lookup just leaves
// positions unresolved, same as before this refactor). GetPacketPathsBulk
// positions unresolved, same as before this refactor). getPacketPathsBulk
// propagates this same helper's error instead; see its own call site.
nodeByPK, _ := db.resolveNodesByPubkey(pubkeys)

Expand All @@ -2884,6 +2888,9 @@ func (db *DB) GetPacketPath(hash string, maxEdgeKm float64) (*PacketPathResponse
nodeByName, _ := db.resolveNodesByName(names) // discarded for the same reason as above

neighborLookup := func(pk string) (neighborEstimate, bool) {
if !estimatesEnabled {
return neighborEstimate{}, false
}
_, nLat, nLon, nCount, nSpread, ok := db.nearestPositionedNeighbor(pk, maxEdgeKm)
if !ok {
return neighborEstimate{}, false
Expand Down Expand Up @@ -3632,7 +3639,7 @@ func (db *DB) GetChannelMessages(channelHash string, limit, offset int, region .
// reply text -- the same farthest-from-first-hearer distance View
// Path shows on its map, computed here as a cheap position-only
// pass (no neighbor-centroid approximation) rather than reusing
// GetPacketPath's heavier per-branch query for every ping.
// getPacketPath's heavier per-branch query for every ping.
observerPubkeys map[string]bool
firstPubkey string
firstTS int64
Expand Down Expand Up @@ -3815,7 +3822,7 @@ func (db *DB) GetChannelMessages(channelHash string, limit, offset int, region .
// Bulk-resolve observer positions too, for the "spread up to Nkm"
// part of the reply -- only for pings that could possibly show one
// (a first-hearer plus at least one other distinct station), and
// deliberately WITHOUT GetPacketPath's neighbor-centroid fallback
// deliberately WITHOUT getPacketPath's neighbor-centroid fallback
// for unpositioned stations: that's a per-node query each, too
// expensive to run for every ping on a page of channel messages.
// A station missing its own GPS fix just doesn't contribute here.
Expand Down Expand Up @@ -3850,7 +3857,7 @@ func (db *DB) GetChannelMessages(channelHash string, limit, offset int, region .
var pk string
var lat, lon sql.NullFloat64
// (0,0) is the ocean off Ghana, not a real fix -- same
// exclusion GetPacketPath applies.
// exclusion getPacketPath applies.
if posRows.Scan(&pk, &lat, &lon) == nil && lat.Valid && lon.Valid && !(lat.Float64 == 0 && lon.Float64 == 0) {
posByPK[pk] = [2]float64{lat.Float64, lon.Float64}
}
Expand Down Expand Up @@ -5712,7 +5719,7 @@ func (db *DB) gpsByPubkeysExact(pubkeys []string) map[string][2]float64 {
continue
}
// (0,0) is the ocean off Ghana, not a real fix -- same
// exclusion GetPacketPath/packetSpreadStats apply.
// exclusion getPacketPath/packetSpreadStats apply.
if lat.Valid && lon.Valid && !(lat.Float64 == 0 && lon.Float64 == 0) {
result[pk] = [2]float64{lat.Float64, lon.Float64}
}
Expand Down
24 changes: 12 additions & 12 deletions cmd/server/db_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -676,7 +676,7 @@ func TestGetPacketPath(t *testing.T) {
db.conn.Exec(`INSERT INTO observations (transmission_id, observer_idx, snr, rssi, path_json, resolved_path, timestamp)
VALUES (1, 2, 4.0, -95, '["aa","bb"]', '["pkAlpha","pkBravo"]', 1736935260)`)

resp, err := db.GetPacketPath("pathtest00000001", 0)
resp, err := db.testPacketPath("pathtest00000001", 0)
if err != nil {
t.Fatal(err)
}
Expand Down Expand Up @@ -737,7 +737,7 @@ func TestGetPacketPath_First(t *testing.T) {
db.conn.Exec(`INSERT INTO observations (transmission_id, observer_idx, snr, rssi, path_json, timestamp)
VALUES (1, 3, 6.0, -90, '["aa","bb"]', 300)`)

resp, err := db.GetPacketPath("pathtest00000007", 0)
resp, err := db.testPacketPath("pathtest00000007", 0)
if err != nil {
t.Fatal(err)
}
Expand Down Expand Up @@ -821,7 +821,7 @@ func TestGetPacketPath_DistanceOmittedWhenApprox(t *testing.T) {
db.conn.Exec(`INSERT INTO observations (transmission_id, observer_idx, snr, rssi, path_json, timestamp)
VALUES (1, 2, 4.0, -95, '["aa","bb"]', 200)`)

resp, err := db.GetPacketPath("pathtest00000011", 0)
resp, err := db.testPacketPath("pathtest00000011", 0)
if err != nil {
t.Fatal(err)
}
Expand Down Expand Up @@ -872,7 +872,7 @@ func TestGetPacketPath_ExcludesNullIsland(t *testing.T) {
db.conn.Exec(`INSERT INTO observations (transmission_id, observer_idx, snr, rssi, path_json, resolved_path, timestamp)
VALUES (1, 1, 9.0, -88, '["aa"]', '["pkZero"]', 1736935200)`)

resp, err := db.GetPacketPath("pathtest00000008", 0)
resp, err := db.testPacketPath("pathtest00000008", 0)
if err != nil {
t.Fatal(err)
}
Expand Down Expand Up @@ -924,7 +924,7 @@ func TestGetPacketPath_FallsBackToSingleNeighborPosition(t *testing.T) {
db.conn.Exec(`INSERT INTO observations (transmission_id, observer_idx, snr, rssi, path_json, resolved_path, timestamp)
VALUES (1, 1, 9.0, -88, '["aa"]', '["pkghost"]', 1736935200)`)

resp, err := db.GetPacketPath("pathtest00000009", 0)
resp, err := db.testPacketPath("pathtest00000009", 0)
if err != nil {
t.Fatal(err)
}
Expand Down Expand Up @@ -999,7 +999,7 @@ func TestGetPacketPath_FallsBackToWeightedNeighborCentroid(t *testing.T) {
// (nearestPositionedNeighbor's maxEdgeKm) is a separate concern with
// its own dedicated tests below and would otherwise drop WeakRepeater
// here, breaking ApproxNeighborCount/ApproxSpreadKm's assertions.
resp, err := db.GetPacketPath("pathtest00000010", 0)
resp, err := db.testPacketPath("pathtest00000010", 0)
if err != nil {
t.Fatal(err)
}
Expand Down Expand Up @@ -1142,7 +1142,7 @@ func TestGetPacketPath_ObserverPositionPrefersOwnGPS(t *testing.T) {
db.conn.Exec(`INSERT INTO observations (transmission_id, observer_idx, snr, rssi, path_json, timestamp)
VALUES (1, 1, 9.0, -88, '[]', 1736935200)`)

resp, err := db.GetPacketPath("pathtest00000004", 0)
resp, err := db.testPacketPath("pathtest00000004", 0)
if err != nil {
t.Fatal(err)
}
Expand Down Expand Up @@ -1185,7 +1185,7 @@ func TestGetPacketPath_ObserverPositionFallsBackToNameMatch(t *testing.T) {
db.conn.Exec(`INSERT INTO observations (transmission_id, observer_idx, snr, rssi, path_json, timestamp)
VALUES (1, 1, 9.0, -88, '[]', 1736935200)`)

resp, err := db.GetPacketPath("pathtest00000005", 0)
resp, err := db.testPacketPath("pathtest00000005", 0)
if err != nil {
t.Fatal(err)
}
Expand Down Expand Up @@ -1220,7 +1220,7 @@ func TestGetPacketPath_ObserverPositionSkipsAmbiguousNameMatch(t *testing.T) {
db.conn.Exec(`INSERT INTO observations (transmission_id, observer_idx, snr, rssi, path_json, timestamp)
VALUES (1, 1, 9.0, -88, '[]', 1736935200)`)

resp, err := db.GetPacketPath("pathtest00000006", 0)
resp, err := db.testPacketPath("pathtest00000006", 0)
if err != nil {
t.Fatal(err)
}
Expand Down Expand Up @@ -1250,7 +1250,7 @@ func TestGetPacketPath_NoResolvedPath(t *testing.T) {
db.conn.Exec(`INSERT INTO observations (transmission_id, observer_idx, snr, rssi, path_json, timestamp)
VALUES (1, 1, 9.0, -88, '["aa"]', 1736935200)`)

resp, err := db.GetPacketPath("pathtest00000002", 0)
resp, err := db.testPacketPath("pathtest00000002", 0)
if err != nil {
t.Fatal(err)
}
Expand Down Expand Up @@ -1290,7 +1290,7 @@ func TestGetPacketPath_SameObserverMultipleObservations(t *testing.T) {
db.conn.Exec(`INSERT INTO observations (transmission_id, observer_idx, snr, rssi, path_json, resolved_path, timestamp)
VALUES (1, 1, 6.0, -100, '["aa"]', '["pkAlpha"]', 1736935260)`)

resp, err := db.GetPacketPath("pathtest00000003", 0)
resp, err := db.testPacketPath("pathtest00000003", 0)
if err != nil {
t.Fatal(err)
}
Expand All @@ -1306,7 +1306,7 @@ func TestGetPacketPath_UnknownHash(t *testing.T) {
db := setupTestDB(t)
defer db.Close()

resp, err := db.GetPacketPath("doesnotexist0000", 0)
resp, err := db.testPacketPath("doesnotexist0000", 0)
if err != nil {
t.Fatal(err)
}
Expand Down
Loading
Loading