diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index ba0d8870..569a6fb5 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -1,2 +1,5 @@ # Line ending normalization (CRLF → LF) — no functional changes b6e4ebf12eba21c78b72978e55052307ca72dbc1 + +# gofmt -w sweep across all Go modules — no functional changes (#1859) +d3a0259925417fb069b860ed8b54e81d02dff175 diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 0d5698c3..8228b867 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -49,6 +49,24 @@ jobs: cmd/server/go.sum cmd/ingestor/go.sum + - name: Enforce gofmt + go vet (issue #1859) + run: | + set -e + # gofmt: every tracked *.go file except the misnamed Dockerfile.go, + # which is a Dockerfile (FROM ...) that gofmt cannot parse. + unformatted=$(gofmt -l $(git ls-files '*.go' | grep -vi 'Dockerfile.go')) + if [ -n "$unformatted" ]; then + echo "::error::gofmt required on these files:" + echo "$unformatted" + exit 1 + fi + echo "gofmt: clean" + # go vet: multi-module repo, so vet each module in turn. + for mod in $(git ls-files '*go.mod' | sed 's#/go.mod##'); do + echo "== go vet $mod ==" + ( cd "$mod" && go vet ./... ) + done + - name: Build and test Go server (with coverage) run: | set -e -o pipefail diff --git a/cmd/ingestor/async_migration_test.go b/cmd/ingestor/async_migration_test.go index dd6f10a4..2a8a942d 100644 --- a/cmd/ingestor/async_migration_test.go +++ b/cmd/ingestor/async_migration_test.go @@ -29,10 +29,10 @@ func waitForStatus(t *testing.T, s *Store, name, want string, timeout time.Durat // TestRunAsyncMigration_PendingThenDone pins the contract for RunAsyncMigration: // -// 1. After calling, the migration name MUST be queryable in the migrations -// table with status `pending_async` IMMEDIATELY (no waiting for fn). -// 2. After fn returns, the status MUST transition to `done`. -// 3. RunAsyncMigration MUST return without blocking on fn. +// 1. After calling, the migration name MUST be queryable in the migrations +// table with status `pending_async` IMMEDIATELY (no waiting for fn). +// 2. After fn returns, the status MUST transition to `done`. +// 3. RunAsyncMigration MUST return without blocking on fn. // // This is the regression test for the recurring "sync migration on large // table blocks ingestor startup" class (#791, #1483, ...). If this test diff --git a/cmd/ingestor/coverage_boost_test.go b/cmd/ingestor/coverage_boost_test.go index 6e754d11..fc2daa88 100644 --- a/cmd/ingestor/coverage_boost_test.go +++ b/cmd/ingestor/coverage_boost_test.go @@ -38,7 +38,8 @@ func TestNodeDaysOrDefault(t *testing.T) { {"custom nodeDays", Config{Retention: &RetentionConfig{NodeDays: 14}}, 14}, {"one day", Config{Retention: &RetentionConfig{NodeDays: 1}}, 1}, } - for _, tt := range tests { + for i := range tests { + tt := &tests[i] t.Run(tt.name, func(t *testing.T) { got := tt.cfg.NodeDaysOrDefault() if got != tt.want { diff --git a/cmd/ingestor/decode_error_log_test.go b/cmd/ingestor/decode_error_log_test.go index 70e56904..ef271046 100644 --- a/cmd/ingestor/decode_error_log_test.go +++ b/cmd/ingestor/decode_error_log_test.go @@ -9,9 +9,10 @@ import ( // TestHandleMessageDecodeErrorLog_PII — issue #1211 round-0 fix shipped without // a test. Asserts the decode-error log line: -// (a) includes structured fields: topic, observer prefix, payload length -// (b) observer substring is at most 8 chars -// (c) full observer ID is NOT present in the output +// +// (a) includes structured fields: topic, observer prefix, payload length +// (b) observer substring is at most 8 chars +// (c) full observer ID is NOT present in the output // // A bare `log.Printf("... observer=%s ...", obs)` would leak the full ID. func TestHandleMessageDecodeErrorLog_PII_Issue1211(t *testing.T) { diff --git a/cmd/ingestor/decoder.go b/cmd/ingestor/decoder.go index 66fdd594..41aa3afc 100644 --- a/cmd/ingestor/decoder.go +++ b/cmd/ingestor/decoder.go @@ -18,26 +18,26 @@ import ( // Route type constants (header bits 1-0) const ( - RouteTransportFlood = 0 - RouteFlood = 1 - RouteDirect = 2 - RouteTransportDirect = 3 + RouteTransportFlood = 0 + RouteFlood = 1 + RouteDirect = 2 + RouteTransportDirect = 3 ) // Payload type constants (header bits 5-2) const ( - PayloadREQ = 0x00 - PayloadRESPONSE = 0x01 - PayloadTXT_MSG = 0x02 - PayloadACK = 0x03 - PayloadADVERT = 0x04 - PayloadGRP_TXT = 0x05 - PayloadGRP_DATA = 0x06 - PayloadANON_REQ = 0x07 - PayloadPATH = 0x08 - PayloadTRACE = 0x09 - PayloadMULTIPART = 0x0A - PayloadCONTROL = 0x0B + PayloadREQ = 0x00 + PayloadRESPONSE = 0x01 + PayloadTXT_MSG = 0x02 + PayloadACK = 0x03 + PayloadADVERT = 0x04 + PayloadGRP_TXT = 0x05 + PayloadGRP_DATA = 0x06 + PayloadANON_REQ = 0x07 + PayloadPATH = 0x08 + PayloadTRACE = 0x09 + PayloadMULTIPART = 0x0A + PayloadCONTROL = 0x0B PayloadRAW_CUSTOM = 0x0F ) @@ -66,11 +66,11 @@ var payloadTypeNames = map[int]string{ // Header is the decoded packet header. type Header struct { - RouteType int `json:"routeType"` - RouteTypeName string `json:"routeTypeName"` - PayloadType int `json:"payloadType"` + RouteType int `json:"routeType"` + RouteTypeName string `json:"routeTypeName"` + PayloadType int `json:"payloadType"` PayloadTypeName string `json:"payloadTypeName"` - PayloadVersion int `json:"payloadVersion"` + PayloadVersion int `json:"payloadVersion"` } // TransportCodes are present on TRANSPORT_FLOOD and TRANSPORT_DIRECT routes. @@ -103,77 +103,77 @@ type AdvertFlags struct { // Payload is a generic decoded payload. Fields are populated depending on type. type Payload struct { - Type string `json:"type"` - DestHash string `json:"destHash,omitempty"` - SrcHash string `json:"srcHash,omitempty"` - MAC string `json:"mac,omitempty"` - EncryptedData string `json:"encryptedData,omitempty"` - ExtraHash string `json:"extraHash,omitempty"` + Type string `json:"type"` + DestHash string `json:"destHash,omitempty"` + SrcHash string `json:"srcHash,omitempty"` + MAC string `json:"mac,omitempty"` + EncryptedData string `json:"encryptedData,omitempty"` + ExtraHash string `json:"extraHash,omitempty"` // Extended ACK fields per firmware 1.16.0 (issue #1610) — // firmware/src/helpers/BaseChatMesh.cpp:218-234. ACK payloads grew from // always-4 bytes to 4/5/6 (4-byte truncated sha256 CRC, optional 1-byte // attempt counter, optional 1-byte RNG byte added in commit a130a95a). // AckLen is the wire payload length; AckAttempt/AckRand are surfaced // only when the sender included them (legacy 4-byte ACKs leave them nil). - AckLen *int `json:"ackLen,omitempty"` - AckAttempt *int `json:"ackAttempt,omitempty"` - AckRand *int `json:"ackRand,omitempty"` - PubKey string `json:"pubKey,omitempty"` - Timestamp uint32 `json:"timestamp,omitempty"` - TimestampISO string `json:"timestampISO,omitempty"` - Signature string `json:"signature,omitempty"` - SignatureValid *bool `json:"signatureValid,omitempty"` - Flags *AdvertFlags `json:"flags,omitempty"` - Lat *float64 `json:"lat,omitempty"` - Lon *float64 `json:"lon,omitempty"` - Name string `json:"name,omitempty"` - Feat1 *int `json:"feat1,omitempty"` - Feat2 *int `json:"feat2,omitempty"` - BatteryMv *int `json:"battery_mv,omitempty"` - TemperatureC *float64 `json:"temperature_c,omitempty"` - ChannelHash int `json:"channelHash,omitempty"` - ChannelHashHex string `json:"channelHashHex,omitempty"` - DecryptionStatus string `json:"decryptionStatus,omitempty"` - Channel string `json:"channel,omitempty"` + AckLen *int `json:"ackLen,omitempty"` + AckAttempt *int `json:"ackAttempt,omitempty"` + AckRand *int `json:"ackRand,omitempty"` + PubKey string `json:"pubKey,omitempty"` + Timestamp uint32 `json:"timestamp,omitempty"` + TimestampISO string `json:"timestampISO,omitempty"` + Signature string `json:"signature,omitempty"` + SignatureValid *bool `json:"signatureValid,omitempty"` + Flags *AdvertFlags `json:"flags,omitempty"` + Lat *float64 `json:"lat,omitempty"` + Lon *float64 `json:"lon,omitempty"` + Name string `json:"name,omitempty"` + Feat1 *int `json:"feat1,omitempty"` + Feat2 *int `json:"feat2,omitempty"` + BatteryMv *int `json:"battery_mv,omitempty"` + TemperatureC *float64 `json:"temperature_c,omitempty"` + ChannelHash int `json:"channelHash,omitempty"` + ChannelHashHex string `json:"channelHashHex,omitempty"` + DecryptionStatus string `json:"decryptionStatus,omitempty"` + Channel string `json:"channel,omitempty"` // GRP_DATA (PAYLOAD_TYPE_GRP_DATA=0x06) inner fields, decoded after // channel decrypt per firmware/src/helpers/BaseChatMesh.cpp:382-385. - DataType *int `json:"dataType,omitempty"` - DataLen *int `json:"dataLen,omitempty"` - DecryptedBlob string `json:"decryptedBlob,omitempty"` - Text string `json:"text,omitempty"` - Sender string `json:"sender,omitempty"` - SenderTimestamp uint32 `json:"sender_timestamp,omitempty"` + DataType *int `json:"dataType,omitempty"` + DataLen *int `json:"dataLen,omitempty"` + DecryptedBlob string `json:"decryptedBlob,omitempty"` + Text string `json:"text,omitempty"` + Sender string `json:"sender,omitempty"` + SenderTimestamp uint32 `json:"sender_timestamp,omitempty"` // ANON_REQ carries the sender's FULL 32-byte public key (not a 1-byte // srcHash like REQ) — MeshCore firmware/src/Mesh.cpp. Surfaced as // srcPubKey so store.go's node indexer picks it up and it resolves to a // node name; frontend also reads legacy ephemeralPubKey for pre-rename // packets (#1864). - SrcPubKey string `json:"srcPubKey,omitempty"` - PathData string `json:"pathData,omitempty"` - SNRValues []float64 `json:"snrValues,omitempty"` - Tag uint32 `json:"tag,omitempty"` - AuthCode uint32 `json:"authCode,omitempty"` - TraceFlags *int `json:"traceFlags,omitempty"` - RawHex string `json:"raw,omitempty"` - Error string `json:"error,omitempty"` + SrcPubKey string `json:"srcPubKey,omitempty"` + PathData string `json:"pathData,omitempty"` + SNRValues []float64 `json:"snrValues,omitempty"` + Tag uint32 `json:"tag,omitempty"` + AuthCode uint32 `json:"authCode,omitempty"` + TraceFlags *int `json:"traceFlags,omitempty"` + RawHex string `json:"raw,omitempty"` + Error string `json:"error,omitempty"` // MULTIPART (PAYLOAD_TYPE_MULTIPART=0x0A) inner fields, decoded per // firmware/src/Mesh.cpp:289 — byte0 = (remaining<<4) | inner_type. - Remaining *int `json:"remaining,omitempty"` - InnerType *int `json:"innerType,omitempty"` - InnerTypeName string `json:"innerTypeName,omitempty"` - InnerAckCrc string `json:"innerAckCrc,omitempty"` + Remaining *int `json:"remaining,omitempty"` + InnerType *int `json:"innerType,omitempty"` + InnerTypeName string `json:"innerTypeName,omitempty"` + InnerAckCrc string `json:"innerAckCrc,omitempty"` // Extended ACK inner fields (issue #1610) — when the multipart inner // blob is a v1.16+ extended ACK (5 or 6 bytes after the byte0 header), // surface the same attempt/rand bytes as the top-level decoder. - InnerAckLen *int `json:"innerAckLen,omitempty"` - InnerAckAttempt *int `json:"innerAckAttempt,omitempty"` - InnerAckRand *int `json:"innerAckRand,omitempty"` - InnerPayload string `json:"innerPayload,omitempty"` + InnerAckLen *int `json:"innerAckLen,omitempty"` + InnerAckAttempt *int `json:"innerAckAttempt,omitempty"` + InnerAckRand *int `json:"innerAckRand,omitempty"` + InnerPayload string `json:"innerPayload,omitempty"` // CONTROL (PAYLOAD_TYPE_CONTROL=0x0B) byte0 flags, per // firmware/src/Mesh.cpp:69 — byte0 high-bit marks zero-hop direct subset. - CtrlFlags string `json:"ctrlFlags,omitempty"` - CtrlZeroHop *bool `json:"ctrlZeroHop,omitempty"` - CtrlLength *int `json:"ctrlLength,omitempty"` + CtrlFlags string `json:"ctrlFlags,omitempty"` + CtrlZeroHop *bool `json:"ctrlZeroHop,omitempty"` + CtrlLength *int `json:"ctrlLength,omitempty"` // CONTROL DISCOVER_REQ / DISCOVER_RESP body fields (#1802). Subtype is // "DISCOVER_REQ" | "DISCOVER_RESP" | "UNKNOWN". For REQ: filter, tag, and // optional since. For RESP: node_type (low nibble of byte0), snr, tag, and @@ -738,9 +738,10 @@ func decodeMultipart(buf []byte) Payload { // // byte0 high nibble is the control subtype (per firmware/src/Mesh.cpp:69 // and firmware/examples/simple_repeater/MyMesh.cpp:773-820): -// 0x80 = CTL_TYPE_NODE_DISCOVER_REQ — body: filter:u8 | tag:u32 LE | since:u32 LE (optional) -// 0x90 = CTL_TYPE_NODE_DISCOVER_RESP — body: snr:i8 | tag:u32 LE | pubkey (32B full, or 8B prefix) -// low nibble of byte0 is node_type +// +// 0x80 = CTL_TYPE_NODE_DISCOVER_REQ — body: filter:u8 | tag:u32 LE | since:u32 LE (optional) +// 0x90 = CTL_TYPE_NODE_DISCOVER_RESP — body: snr:i8 | tag:u32 LE | pubkey (32B full, or 8B prefix) +// low nibble of byte0 is node_type // // The legacy CtrlZeroHop bool (bit7 of byte0) is retained for backwards // compatibility — it is misleading (bit7 is also set for DISCOVER_RESP) @@ -860,11 +861,11 @@ func decodeAnonReq(buf []byte) Payload { return Payload{Type: "ANON_REQ", Error: "too short", RawHex: hex.EncodeToString(buf)} } return Payload{ - Type: "ANON_REQ", - DestHash: hex.EncodeToString(buf[0:1]), - SrcPubKey: hex.EncodeToString(buf[1:33]), - MAC: hex.EncodeToString(buf[33:35]), - EncryptedData: hex.EncodeToString(buf[35:]), + Type: "ANON_REQ", + DestHash: hex.EncodeToString(buf[0:1]), + SrcPubKey: hex.EncodeToString(buf[1:33]), + MAC: hex.EncodeToString(buf[33:35]), + EncryptedData: hex.EncodeToString(buf[35:]), } } @@ -1204,7 +1205,9 @@ func sanitizeName(s string) string { // advertRole returns a stable role label for an advert. Follows firmware // ADV_TYPE_* constants in firmware/src/helpers/AdvertDataHelpers.h:7-12: -// 0 NONE, 1 CHAT, 2 REPEATER, 3 ROOM, 4 SENSOR, 5-15 FUTURE. +// +// 0 NONE, 1 CHAT, 2 REPEATER, 3 ROOM, 4 SENSOR, 5-15 FUTURE. +// // Previously this coerced both 0 (NONE) and 5-15 (FUTURE) to "companion", // silently relabelling unknown/reserved types — see issue #1279 P1 #3. func advertRole(f *AdvertFlags) string { diff --git a/cmd/ingestor/decoder_test.go b/cmd/ingestor/decoder_test.go index 1f942366..5f6d7116 100644 --- a/cmd/ingestor/decoder_test.go +++ b/cmd/ingestor/decoder_test.go @@ -168,8 +168,8 @@ func TestDecodeAdvertFull(t *testing.T) { signature := strings.Repeat("BB", 64) // flags: 0x92 = repeater(2) | hasLocation(0x10) | hasName(0x80) flags := "92" - lat := "40933402" // ~37.0 - lon := "E0E6B8F8" // ~-122.1 + lat := "40933402" // ~37.0 + lon := "E0E6B8F8" // ~-122.1 name := "546573744E6F6465" // "TestNode" hex := "1200" + pubkey + timestamp + signature + flags + lat + lon + name @@ -230,7 +230,7 @@ func TestDecodeAdvertFull(t *testing.T) { func TestDecodeAdvertTypeEnums(t *testing.T) { makeAdvert := func(flagsByte byte) *DecodedPacket { hex := "1200" + strings.Repeat("AA", 32) + "00000000" + strings.Repeat("BB", 64) + - strings.ToUpper(string([]byte{hexDigit(flagsByte>>4), hexDigit(flagsByte & 0x0f)})) + strings.ToUpper(string([]byte{hexDigit(flagsByte >> 4), hexDigit(flagsByte & 0x0f)})) pkt, err := DecodePacket(hex, nil, false) if err != nil { t.Fatal(err) @@ -573,8 +573,8 @@ func TestDecodeTraceValid(t *testing.T) { // tag(4) + authCode(4) + flags(1) + pathData binary.LittleEndian.PutUint32(buf[0:4], 1) // tag = 1 binary.LittleEndian.PutUint32(buf[4:8], 0xDEADBEEF) // authCode - buf[8] = 0x02 // flags - buf[9] = 0xAA // path data + buf[8] = 0x02 // flags + buf[9] = 0xAA // path data p := decodeTrace(buf) if p.Error != "" { t.Errorf("unexpected error: %s", p.Error) @@ -946,7 +946,7 @@ func TestComputeContentHashTransportBeyondBuffer(t *testing.T) { // Transport route (0x00 = TRANSPORT_FLOOD) with path claiming some bytes // header=0x00, transport(4), pathByte=0x02 (2 hops, 1-byte hash) // offset=1+4+1+2=8, buffer needs to be >= 8 - hex := "00" + "AABB" + "CCDD" + "02" + strings.Repeat("CC", 6) // 20 chars = 10 bytes + hex := "00" + "AABB" + "CCDD" + "02" + strings.Repeat("CC", 6) // 20 chars = 10 bytes hash := ComputeContentHash(hex) if len(hash) != 16 { t.Errorf("hash length=%d, want 16", len(hash)) @@ -1032,8 +1032,9 @@ func TestComputeContentHashMatchesFirmware(t *testing.T) { // Packet: header=0x25 (FLOOD route=1, payload_type=TRACE=0x09), pathByte=0x02 // (2 hops, 1-byte hash), path=[AA,BB], payload=[DE,AD,BE,EF]. // Hash input: [0x09, 0x02, 0x00, 0xDE, 0xAD, 0xBE, 0xEF] -// → SHA256 = b1baaf3bf0d0726c2672b1ec9e2665dc... -// → first 16 hex chars = "b1baaf3bf0d0726c" +// +// → SHA256 = b1baaf3bf0d0726c2672b1ec9e2665dc... +// → first 16 hex chars = "b1baaf3bf0d0726c" func TestComputeContentHashTraceGoldenValue(t *testing.T) { // TRACE packet: header byte 0x25 = payload_type 9 (TRACE), route_type 1 (FLOOD) // pathByte 0x02 = hash_size 1, hash_count 2 @@ -1989,10 +1990,11 @@ func TestDecodeTracePayloadFailSetsAnomaly(t *testing.T) { // extracted by server but never written into decoded_json by ingestor). // // Packet 26022FF8116A23A80000000001C0DE1000DEDE: -// header 0x26 → TRACE (pt=9), DIRECT (rt=2) -// pathByte 0x02 → hash_size=1, hash_count=2 -// header path: 2F F8 → SNR = [int8(0x2F)/4, int8(0xF8)/4] = [11.75, -2.0] -// payload (15B): tag=116A23A8 auth=00000000 flags=0x01 pathData=C0DE1000DEDE +// +// header 0x26 → TRACE (pt=9), DIRECT (rt=2) +// pathByte 0x02 → hash_size=1, hash_count=2 +// header path: 2F F8 → SNR = [int8(0x2F)/4, int8(0xF8)/4] = [11.75, -2.0] +// payload (15B): tag=116A23A8 auth=00000000 flags=0x01 pathData=C0DE1000DEDE func TestDecodeTraceExtractsSNRValues(t *testing.T) { pkt, err := DecodePacket("26022FF8116A23A80000000001C0DE1000DEDE", nil, false) if err != nil { diff --git a/cmd/ingestor/main_test.go b/cmd/ingestor/main_test.go index e0699602..9d241f46 100644 --- a/cmd/ingestor/main_test.go +++ b/cmd/ingestor/main_test.go @@ -110,9 +110,9 @@ type mockMessage struct { retained bool } -func (m *mockMessage) Duplicate() bool { return false } -func (m *mockMessage) Qos() byte { return 0 } -func (m *mockMessage) Retained() bool { return m.retained } +func (m *mockMessage) Duplicate() bool { return false } +func (m *mockMessage) Qos() byte { return 0 } +func (m *mockMessage) Retained() bool { return m.retained } func (m *mockMessage) Topic() string { return m.topic } func (m *mockMessage) MessageID() uint16 { return 0 } func (m *mockMessage) Payload() []byte { return m.payload } @@ -599,10 +599,10 @@ func TestLoadChannelKeysHashChannelsNormalization(t *testing.T) { cfg := &Config{ HashChannels: []string{ - "NoPound", // should become #NoPound - "#HasPound", // stays #HasPound - " Spaced ", // trimmed → #Spaced - "", // skipped + "NoPound", // should become #NoPound + "#HasPound", // stays #HasPound + " Spaced ", // trimmed → #Spaced + "", // skipped }, } diff --git a/cmd/ingestor/mqtt_watchdog_r1_test.go b/cmd/ingestor/mqtt_watchdog_r1_test.go index 07bb2a87..7af0781a 100644 --- a/cmd/ingestor/mqtt_watchdog_r1_test.go +++ b/cmd/ingestor/mqtt_watchdog_r1_test.go @@ -231,7 +231,7 @@ func TestMQTTStallWatchdog_RecoveryEmitOnce(t *testing.T) { waitFor(t, &mu, &emits, 1, 2*time.Second) // Source recovers: a recent message arrives. atomic.StoreInt64(&s.LastMessageUnix, now.Add(30*time.Second).Unix()) - tick <- now.Add(60 * time.Second) // → recovery INFO + tick <- now.Add(60 * time.Second) // → recovery INFO waitFor(t, &mu, &emits, 2, 2*time.Second) tick <- now.Add(120 * time.Second) // → silent tick <- now.Add(180 * time.Second) // → silent diff --git a/cmd/ingestor/path_resolver.go b/cmd/ingestor/path_resolver.go index 7ca94479..06cb92ac 100644 --- a/cmd/ingestor/path_resolver.go +++ b/cmd/ingestor/path_resolver.go @@ -134,12 +134,13 @@ func loadNeighborGraph(db *sql.DB) (*NeighborGraph, error) { // does not revisit a node). // // Behavior matrix: -// len(candidates) | anchor | graph | result -// 0 | — | — | nil -// 1 | — | — | candidates[0] -// >1 | "" or no graph|— | nil -// >1 | non-empty | set | unique adjacent candidate -// (or nil if 0 or >1 survive) +// +// len(candidates) | anchor | graph | result +// 0 | — | — | nil +// 1 | — | — | candidates[0] +// >1 | "" or no graph|— | nil +// >1 | non-empty | set | unique adjacent candidate +// (or nil if 0 or >1 survive) func resolveHopWithContext(hop string, anchor string, graph *NeighborGraph, idx prefixIndex, exclude map[string]struct{}) *string { if idx == nil { return nil diff --git a/cmd/ingestor/source_status.go b/cmd/ingestor/source_status.go index d18637de..fc9c37a9 100644 --- a/cmd/ingestor/source_status.go +++ b/cmd/ingestor/source_status.go @@ -50,9 +50,9 @@ type sourceStatusState struct { // 5-minute sliding window: per-second buckets keyed by unix second. // Stored as parallel arrays so we can both zero-out a stale slot AND // know whether a slot's contents are still inside the window. - ringMu sync.Mutex - ringSec [300]int64 // unix second this slot represents (0 = unused) - ringCount [300]int64 // packets received in that second + ringMu sync.Mutex + ringSec [300]int64 // unix second this slot represents (0 = unused) + ringCount [300]int64 // packets received in that second // lastError is rare-write/rare-read so a plain mutex is fine. errMu sync.RWMutex diff --git a/cmd/ingestor/stats_file_bench_test.go b/cmd/ingestor/stats_file_bench_test.go index 0702dfde..c7946c2a 100644 --- a/cmd/ingestor/stats_file_bench_test.go +++ b/cmd/ingestor/stats_file_bench_test.go @@ -34,7 +34,6 @@ func TestStatsFileWriterBench_Sanity(t *testing.T) { } } - // BenchmarkParseProcSelfIOInto measures the ingestor-side /proc/self/io // parser on a representative payload (Carmack must-fix #3). Tracks // allocations to verify the shared perfio.ParseProcIO path doesn't diff --git a/cmd/ingestor/stats_file_test.go b/cmd/ingestor/stats_file_test.go index 29ba4b22..4a488f26 100644 --- a/cmd/ingestor/stats_file_test.go +++ b/cmd/ingestor/stats_file_test.go @@ -108,8 +108,9 @@ func TestStatsFileWriter_PublishesProcIO(t *testing.T) { // // This test pre-plants a symlink at `path` pointing to an unrelated target // file and asserts: -// (a) post-write, path is a regular file (not a symlink), and -// (b) the original target's contents are unchanged. +// +// (a) post-write, path is a regular file (not a symlink), and +// (b) the original target's contents are unchanged. // // If a future refactor swaps os.Rename for something that follows the // destination symlink (e.g. ioutil.WriteFile, or an open(path, O_WRONLY) diff --git a/cmd/server/analytics_recomputer.go b/cmd/server/analytics_recomputer.go index 88ac631e..57ff5f76 100644 --- a/cmd/server/analytics_recomputer.go +++ b/cmd/server/analytics_recomputer.go @@ -19,13 +19,13 @@ import ( // in an atomic.Value, refreshed periodically by a background goroutine. // // Lifecycle: -// 1. Construct via newAnalyticsRecomputer(...) -// 2. Call Start() — runs initial compute synchronously, then launches -// the recompute goroutine. Initial compute is synchronous so the -// first Load() after Start returns never sees a nil cache. -// 3. Call Load() any number of times concurrently — never blocks -// beyond an atomic-pointer load. -// 4. Call Stop() to terminate the background goroutine cleanly. +// 1. Construct via newAnalyticsRecomputer(...) +// 2. Call Start() — runs initial compute synchronously, then launches +// the recompute goroutine. Initial compute is synchronous so the +// first Load() after Start returns never sees a nil cache. +// 3. Call Load() any number of times concurrently — never blocks +// beyond an atomic-pointer load. +// 4. Call Stop() to terminate the background goroutine cleanly. // // Compute func is called WITHOUT any lock held by this struct, so it // may freely take any application-level locks it needs. @@ -172,15 +172,15 @@ func (r *analyticsRecomputer) ComputeRuns() int64 { // per-endpoint recompute interval from config.json. Zero values fall // back to the defaultInterval passed to StartAnalyticsRecomputers. type AnalyticsRecomputeIntervals struct { - Topology time.Duration - RF time.Duration - Distance time.Duration - Channels time.Duration - HashCollisions time.Duration - HashSizes time.Duration - Roles time.Duration - ObserversClockSkew time.Duration - NodesClockSkew time.Duration + Topology time.Duration + RF time.Duration + Distance time.Duration + Channels time.Duration + HashCollisions time.Duration + HashSizes time.Duration + Roles time.Duration + ObserversClockSkew time.Duration + NodesClockSkew time.Duration } func pickInterval(override, def time.Duration) time.Duration { diff --git a/cmd/server/analytics_warmup_1659.go b/cmd/server/analytics_warmup_1659.go index 0709d5fc..37585c12 100644 --- a/cmd/server/analytics_warmup_1659.go +++ b/cmd/server/analytics_warmup_1659.go @@ -19,28 +19,28 @@ // // Two correctness invariants: // -// 1. (#1688 munger #5) Only mark first-pass-done when BOTH: -// a. a recomputer pass has completed, AND -// b. the chunked loader has finished (s.LoadComplete()). -// The gate's `readyGate` callback is wired by -// StartAnalyticsRecomputers to `store.LoadComplete`. Passes that -// complete while loadComplete is still false leave the gate in -// the warming-up state; the NEXT pass after loadComplete flips -// true is the one that opens the gate. +// 1. (#1688 munger #5) Only mark first-pass-done when BOTH: +// a. a recomputer pass has completed, AND +// b. the chunked loader has finished (s.LoadComplete()). +// The gate's `readyGate` callback is wired by +// StartAnalyticsRecomputers to `store.LoadComplete`. Passes that +// complete while loadComplete is still false leave the gate in +// the warming-up state; the NEXT pass after loadComplete flips +// true is the one that opens the gate. // -// 2. (#1688 munger #2 + kent-beck #2) The gate MUST lift in bounded -// time. If compute() panics on every pass, hangs indefinitely, -// or returns nil forever, an unguarded gate would leave the -// 503 banner permanent. Two safeguards: -// a. compute() panics are already caught by runOnce()'s -// defer recover(); we additionally call markFirstPassDone -// on EVERY pass (even nil-result), so a recomputer that -// returns nil but doesn't panic still flips the gate. -// b. A hard fallback timeout (warmupForceTimeout, 60s by -// default) elapsed since the recomputer was constructed -// forces IsWarmingUp_1659() to false — degraded mode -// (serve whatever cache exists, possibly empty) is -// strictly better than a permanent 503. +// 2. (#1688 munger #2 + kent-beck #2) The gate MUST lift in bounded +// time. If compute() panics on every pass, hangs indefinitely, +// or returns nil forever, an unguarded gate would leave the +// 503 banner permanent. Two safeguards: +// a. compute() panics are already caught by runOnce()'s +// defer recover(); we additionally call markFirstPassDone +// on EVERY pass (even nil-result), so a recomputer that +// returns nil but doesn't panic still flips the gate. +// b. A hard fallback timeout (warmupForceTimeout, 60s by +// default) elapsed since the recomputer was constructed +// forces IsWarmingUp_1659() to false — degraded mode +// (serve whatever cache exists, possibly empty) is +// strictly better than a permanent 503. // // Concurrency (#1688 munger #3): // diff --git a/cmd/server/area_filter_test.go b/cmd/server/area_filter_test.go index e08082c0..25c07d90 100644 --- a/cmd/server/area_filter_test.go +++ b/cmd/server/area_filter_test.go @@ -77,28 +77,28 @@ func TestGetNodePubkeysInArea_Polygon(t *testing.T) { func newTestStoreWithDB(t *testing.T, db *DB, cfg *Config) *PacketStore { t.Helper() return &PacketStore{ - db: db, - config: cfg, - byNode: make(map[string][]*StoreTx), - byTxID: make(map[int]*StoreTx), - byObsID: make(map[int]*StoreObs), - byObserver: make(map[string][]*StoreObs), - byHash: make(map[string]*StoreTx), - byPayloadType: make(map[int][]*StoreTx), - nodeHashes: make(map[string]map[string]bool), - byPathHop: make(map[string][]*StoreTx), - advertPubkeys: make(map[string]int), - rfCache: make(map[string]*cachedResult), - topoCache: make(map[string]*cachedResult), - hashCache: make(map[string]*cachedResult), - collisionCache: make(map[string]*cachedResult), - chanCache: make(map[string]*cachedResult), - distCache: make(map[string]*cachedResult), - subpathCache: make(map[string]*cachedResult), + db: db, + config: cfg, + byNode: make(map[string][]*StoreTx), + byTxID: make(map[int]*StoreTx), + byObsID: make(map[int]*StoreObs), + byObserver: make(map[string][]*StoreObs), + byHash: make(map[string]*StoreTx), + byPayloadType: make(map[int][]*StoreTx), + nodeHashes: make(map[string]map[string]bool), + byPathHop: make(map[string][]*StoreTx), + advertPubkeys: make(map[string]int), + rfCache: make(map[string]*cachedResult), + topoCache: make(map[string]*cachedResult), + hashCache: make(map[string]*cachedResult), + collisionCache: make(map[string]*cachedResult), + chanCache: make(map[string]*cachedResult), + distCache: make(map[string]*cachedResult), + subpathCache: make(map[string]*cachedResult), regionObsCache: make(map[string]map[string]bool), areaNodeCache: make(map[string]map[string]bool), areaNodeCacheTimes: make(map[string]time.Time), - rfCacheTTL: 15 * time.Second, + rfCacheTTL: 15 * time.Second, } } diff --git a/cmd/server/cache_invalidation_test.go b/cmd/server/cache_invalidation_test.go index 148ffd57..39b7c694 100644 --- a/cmd/server/cache_invalidation_test.go +++ b/cmd/server/cache_invalidation_test.go @@ -45,7 +45,7 @@ func cachePopulated(s *PacketStore) map[string]bool { "topo": len(s.topoCache) > 0, "hash": len(s.hashCache) > 0, "collision": len(s.collisionCache) > 0, - "chan": len(s.chanCache) > 0, + "chan": len(s.chanCache) > 0, "dist": len(s.distCache) > 0, "subpath": len(s.subpathCache) > 0, } diff --git a/cmd/server/cdn_detection.go b/cmd/server/cdn_detection.go index fca4c7d4..35853005 100644 --- a/cmd/server/cdn_detection.go +++ b/cmd/server/cdn_detection.go @@ -46,10 +46,10 @@ var cdnWarned atomic.Bool // using them as a CDN signal produces a false positive on every // reverse-proxied install (issue #1561 round-1 review). var cdnHeaders = []string{ - "CF-Connecting-IP", // Cloudflare - "CF-Ray", // Cloudflare - "Fastly-Client-IP", // Fastly - "True-Client-IP", // Akamai (also set by Cloudflare Enterprise) + "CF-Connecting-IP", // Cloudflare + "CF-Ray", // Cloudflare + "Fastly-Client-IP", // Fastly + "True-Client-IP", // Akamai (also set by Cloudflare Enterprise) } // cdnDetectionMiddleware inspects each incoming request for CDN diff --git a/cmd/server/channel_analytics_test.go b/cmd/server/channel_analytics_test.go index cc1f47ed..b2a44ad2 100644 --- a/cmd/server/channel_analytics_test.go +++ b/cmd/server/channel_analytics_test.go @@ -11,26 +11,26 @@ var _ = time.Second // suppress unused import // Helper to create a minimal PacketStore with GRP_TXT packets for channel analytics testing. func newChannelTestStore(packets []*StoreTx) *PacketStore { ps := &PacketStore{ - packets: packets, - byHash: make(map[string]*StoreTx), - byTxID: make(map[int]*StoreTx), - byObsID: make(map[int]*StoreObs), - byObserver: make(map[string][]*StoreObs), - byNode: make(map[string][]*StoreTx), - byPathHop: make(map[string][]*StoreTx), - nodeHashes: make(map[string]map[string]bool), - byPayloadType: make(map[int][]*StoreTx), - rfCache: make(map[string]*cachedResult), - topoCache: make(map[string]*cachedResult), - hashCache: make(map[string]*cachedResult), + packets: packets, + byHash: make(map[string]*StoreTx), + byTxID: make(map[int]*StoreTx), + byObsID: make(map[int]*StoreObs), + byObserver: make(map[string][]*StoreObs), + byNode: make(map[string][]*StoreTx), + byPathHop: make(map[string][]*StoreTx), + nodeHashes: make(map[string]map[string]bool), + byPayloadType: make(map[int][]*StoreTx), + rfCache: make(map[string]*cachedResult), + topoCache: make(map[string]*cachedResult), + hashCache: make(map[string]*cachedResult), collisionCache: make(map[string]*cachedResult), - chanCache: make(map[string]*cachedResult), - distCache: make(map[string]*cachedResult), - subpathCache: make(map[string]*cachedResult), - spIndex: make(map[string]int), - spTxIndex: make(map[string][]*StoreTx), - advertPubkeys: make(map[string]int), - clockSkew: NewClockSkewEngine(), + chanCache: make(map[string]*cachedResult), + distCache: make(map[string]*cachedResult), + subpathCache: make(map[string]*cachedResult), + spIndex: make(map[string]int), + spTxIndex: make(map[string][]*StoreTx), + advertPubkeys: make(map[string]int), + clockSkew: NewClockSkewEngine(), } ps.byPayloadType[5] = packets return ps @@ -73,8 +73,8 @@ func TestComputeAnalyticsChannels_MergesEncryptedAndDecrypted(t *testing.T) { packets := []*StoreTx{ makeGrpTx(129, "#wardriving", "hello", "alice"), makeGrpTx(129, "#wardriving", "world", "bob"), - makeGrpTx(129, "", "", ""), // encrypted — no channel name - makeGrpTx(129, "", "", ""), // encrypted + makeGrpTx(129, "", "", ""), // encrypted — no channel name + makeGrpTx(129, "", "", ""), // encrypted } store := newChannelTestStore(packets) @@ -103,7 +103,7 @@ func TestComputeAnalyticsChannels_RejectsRainbowTableMismatch(t *testing.T) { // Hash 72 is NOT the correct hash for #wardriving (which is 129). // This simulates a rainbow-table collision/mismatch. packets := []*StoreTx{ - makeGrpTx(72, "#wardriving", "ghost", "eve"), // mismatch: hash 72 != wardriving's real hash + makeGrpTx(72, "#wardriving", "ghost", "eve"), // mismatch: hash 72 != wardriving's real hash makeGrpTx(129, "#wardriving", "real", "alice"), // correct match } diff --git a/cmd/server/channels_message_order_1366_test.go b/cmd/server/channels_message_order_1366_test.go index a4f6c149..0fde9bfb 100644 --- a/cmd/server/channels_message_order_1366_test.go +++ b/cmd/server/channels_message_order_1366_test.go @@ -41,8 +41,8 @@ func TestChannelMessages_TimestampUsesLatestSeen(t *testing.T) { // One transmission with two observations: T0 (7h ago) and T1 (5m ago). db.conn.Exec(`INSERT INTO transmissions (raw_hex, hash, first_seen, route_type, payload_type, decoded_json, channel_hash) VALUES ('AA01', 'hash_repeated_msg', ?, 1, 5, - '{"type":"CHAN","channel":"#test","text":"Heartbeat: ping","sender":"Heartbeat","sender_timestamp":` + - strconv.FormatInt(firstSeenEpoch, 10) + `}', + '{"type":"CHAN","channel":"#test","text":"Heartbeat: ping","sender":"Heartbeat","sender_timestamp":`+ + strconv.FormatInt(firstSeenEpoch, 10)+`}', '#test')`, firstSeen) db.conn.Exec(`INSERT INTO observations (transmission_id, observer_idx, snr, rssi, path_json, timestamp) VALUES (1, 1, 10.0, -90, '["aa"]', ?)`, firstSeenEpoch) @@ -105,8 +105,8 @@ func TestChannelMessages_TimestampNotSenderTimestamp(t *testing.T) { VALUES ('obsX', 'ObsX', 'SJC', ?, '2026-01-01T00:00:00Z', 1)`, firstSeen) db.conn.Exec(`INSERT INTO transmissions (raw_hex, hash, first_seen, route_type, payload_type, decoded_json, channel_hash) VALUES ('BB01', 'hash_bad_clock', ?, 1, 5, - '{"type":"CHAN","channel":"#bad","text":"Alice: ping","sender":"Alice","sender_timestamp":` + - strconv.FormatInt(badSenderTs, 10) + `}', + '{"type":"CHAN","channel":"#bad","text":"Alice: ping","sender":"Alice","sender_timestamp":`+ + strconv.FormatInt(badSenderTs, 10)+`}', '#bad')`, firstSeen) db.conn.Exec(`INSERT INTO observations (transmission_id, observer_idx, snr, rssi, path_json, timestamp) VALUES (1, 1, 10.0, -90, '["aa"]', ?)`, firstSeenEpoch) diff --git a/cmd/server/chunked_load_test.go b/cmd/server/chunked_load_test.go index 512a8da3..c603acd3 100644 --- a/cmd/server/chunked_load_test.go +++ b/cmd/server/chunked_load_test.go @@ -84,7 +84,8 @@ func TestLoadChunked_FirstChunkReadyBeforeComplete(t *testing.T) { } // (b) Middleware stamps X-CoreScope-Load-Status correctly across the -// loading→ready transition. +// +// loading→ready transition. func TestLoadStatusMiddleware_HeaderTransition(t *testing.T) { store := openChunkedTestStore(t, 100) defer store.db.conn.Close() @@ -115,7 +116,8 @@ func TestLoadStatusMiddleware_HeaderTransition(t *testing.T) { } // (c) LoadChunked honors the chunkSize argument — progress callback -// fires once per chunk. +// +// fires once per chunk. func TestLoadChunked_ChunkSizeHonored(t *testing.T) { store := openChunkedTestStore(t, 2500) defer store.db.conn.Close() diff --git a/cmd/server/clock_skew.go b/cmd/server/clock_skew.go index 3aea874b..51bdf9cd 100644 --- a/cmd/server/clock_skew.go +++ b/cmd/server/clock_skew.go @@ -13,10 +13,10 @@ import ( type SkewSeverity string const ( - SkewOK SkewSeverity = "ok" // < 5 min - SkewWarning SkewSeverity = "warning" // 5 min – 1 hour - SkewCritical SkewSeverity = "critical" // 1 hour – 30 days - SkewAbsurd SkewSeverity = "absurd" // > 30 days + SkewOK SkewSeverity = "ok" // < 5 min + SkewWarning SkewSeverity = "warning" // 5 min – 1 hour + SkewCritical SkewSeverity = "critical" // 1 hour – 30 days + SkewAbsurd SkewSeverity = "absurd" // > 30 days SkewNoClock SkewSeverity = "no_clock" // > 365 days — uninitialized RTC SkewBimodalClock SkewSeverity = "bimodal_clock" // mixed good+bad recent samples (flaky RTC) ) @@ -105,41 +105,41 @@ func classifySkew(absSkewSec float64) SkewSeverity { // skewSample is a single raw skew measurement from one advert observation. type skewSample struct { - advertTS int64 // node's advert Unix timestamp - observedTS int64 // observation Unix timestamp - observerID string // which observer saw this - hash string // transmission hash (for multi-observer grouping) + advertTS int64 // node's advert Unix timestamp + observedTS int64 // observation Unix timestamp + observerID string // which observer saw this + hash string // transmission hash (for multi-observer grouping) } // ObserverCalibration holds the computed clock offset for an observer. type ObserverCalibration struct { ObserverID string `json:"observerID"` - OffsetSec float64 `json:"offsetSec"` // positive = observer clock ahead - Samples int `json:"samples"` // number of multi-observer packets used + OffsetSec float64 `json:"offsetSec"` // positive = observer clock ahead + Samples int `json:"samples"` // number of multi-observer packets used } // NodeClockSkew is the API response for a single node's clock skew data. type NodeClockSkew struct { - Pubkey string `json:"pubkey"` - MeanSkewSec float64 `json:"meanSkewSec"` // corrected mean skew (positive = node ahead) - MedianSkewSec float64 `json:"medianSkewSec"` // corrected median skew - LastSkewSec float64 `json:"lastSkewSec"` // most recent corrected skew - RecentMedianSkewSec float64 `json:"recentMedianSkewSec"` // median across most-recent samples (drives severity, see #789) - DriftPerDaySec float64 `json:"driftPerDaySec"` // linear drift rate (sec/day) - Severity SkewSeverity `json:"severity"` - SampleCount int `json:"sampleCount"` - Calibrated bool `json:"calibrated"` // true if observer calibration was applied - LastAdvertTS int64 `json:"lastAdvertTS"` // most recent advert timestamp - LastObservedTS int64 `json:"lastObservedTS"` // most recent observation timestamp - Samples []SkewSample `json:"samples,omitempty"` // time-series for sparklines - GoodFraction float64 `json:"goodFraction"` // fraction of recent samples with |skew| <= 1h - RecentBadSampleCount int `json:"recentBadSampleCount"` // count of recent samples with |skew| > 1h - RecentBadSamples []BadSample `json:"recentBadSamples,omitempty"` // #1094: per-bad-sample evidence (hash + bad advertTS) - RecentSampleCount int `json:"recentSampleCount"` // total recent samples in window - RecentHashEvidence []HashEvidence `json:"recentHashEvidence,omitempty"` - CalibrationSummary *CalibrationSummary `json:"calibrationSummary,omitempty"` - NodeName string `json:"nodeName,omitempty"` // populated in fleet responses - NodeRole string `json:"nodeRole,omitempty"` // populated in fleet responses + Pubkey string `json:"pubkey"` + MeanSkewSec float64 `json:"meanSkewSec"` // corrected mean skew (positive = node ahead) + MedianSkewSec float64 `json:"medianSkewSec"` // corrected median skew + LastSkewSec float64 `json:"lastSkewSec"` // most recent corrected skew + RecentMedianSkewSec float64 `json:"recentMedianSkewSec"` // median across most-recent samples (drives severity, see #789) + DriftPerDaySec float64 `json:"driftPerDaySec"` // linear drift rate (sec/day) + Severity SkewSeverity `json:"severity"` + SampleCount int `json:"sampleCount"` + Calibrated bool `json:"calibrated"` // true if observer calibration was applied + LastAdvertTS int64 `json:"lastAdvertTS"` // most recent advert timestamp + LastObservedTS int64 `json:"lastObservedTS"` // most recent observation timestamp + Samples []SkewSample `json:"samples,omitempty"` // time-series for sparklines + GoodFraction float64 `json:"goodFraction"` // fraction of recent samples with |skew| <= 1h + RecentBadSampleCount int `json:"recentBadSampleCount"` // count of recent samples with |skew| > 1h + RecentBadSamples []BadSample `json:"recentBadSamples,omitempty"` // #1094: per-bad-sample evidence (hash + bad advertTS) + RecentSampleCount int `json:"recentSampleCount"` // total recent samples in window + RecentHashEvidence []HashEvidence `json:"recentHashEvidence,omitempty"` + CalibrationSummary *CalibrationSummary `json:"calibrationSummary,omitempty"` + NodeName string `json:"nodeName,omitempty"` // populated in fleet responses + NodeRole string `json:"nodeRole,omitempty"` // populated in fleet responses } // SkewSample is a single (timestamp, skew) point for sparkline rendering. @@ -159,26 +159,26 @@ type BadSample struct { // HashEvidenceObserver is one observer's contribution to a per-hash evidence entry. type HashEvidenceObserver struct { - ObserverID string `json:"observerID"` - ObserverName string `json:"observerName"` - RawSkewSec float64 `json:"rawSkewSec"` - CorrectedSkewSec float64 `json:"correctedSkewSec"` + ObserverID string `json:"observerID"` + ObserverName string `json:"observerName"` + RawSkewSec float64 `json:"rawSkewSec"` + CorrectedSkewSec float64 `json:"correctedSkewSec"` ObserverOffsetSec float64 `json:"observerOffsetSec"` - Calibrated bool `json:"calibrated"` + Calibrated bool `json:"calibrated"` } // HashEvidence is per-hash clock skew evidence showing individual observer contributions. type HashEvidence struct { - Hash string `json:"hash"` - Observers []HashEvidenceObserver `json:"observers"` - MedianCorrectedSkewSec float64 `json:"medianCorrectedSkewSec"` - Timestamp int64 `json:"timestamp"` + Hash string `json:"hash"` + Observers []HashEvidenceObserver `json:"observers"` + MedianCorrectedSkewSec float64 `json:"medianCorrectedSkewSec"` + Timestamp int64 `json:"timestamp"` } // CalibrationSummary counts how many samples were corrected via observer calibration. type CalibrationSummary struct { - TotalSamples int `json:"totalSamples"` - CalibratedSamples int `json:"calibratedSamples"` + TotalSamples int `json:"totalSamples"` + CalibratedSamples int `json:"calibratedSamples"` UncalibratedSamples int `json:"uncalibratedSamples"` } @@ -191,31 +191,31 @@ type txSkewResult = map[string]*NodeClockSkew // ClockSkewEngine computes and caches clock skew data for nodes and observers. type ClockSkewEngine struct { - mu sync.RWMutex - observerOffsets map[string]float64 // observerID → calibrated offset (seconds) - observerSamples map[string]int // observerID → number of multi-observer packets used - nodeSkew txSkewResult - hashEvidence map[string][]hashEvidenceEntry // hash → per-observer raw/corrected data - lastComputed time.Time - computeInterval time.Duration + mu sync.RWMutex + observerOffsets map[string]float64 // observerID → calibrated offset (seconds) + observerSamples map[string]int // observerID → number of multi-observer packets used + nodeSkew txSkewResult + hashEvidence map[string][]hashEvidenceEntry // hash → per-observer raw/corrected data + lastComputed time.Time + computeInterval time.Duration } // hashEvidenceEntry stores raw evidence per observer per hash, cached during Recompute. type hashEvidenceEntry struct { - observerID string - rawSkew float64 - corrected float64 - offset float64 - calibrated bool - observedTS int64 + observerID string + rawSkew float64 + corrected float64 + offset float64 + calibrated bool + observedTS int64 } func NewClockSkewEngine() *ClockSkewEngine { return &ClockSkewEngine{ - observerOffsets: make(map[string]float64), + observerOffsets: make(map[string]float64), observerSamples: make(map[string]int), - nodeSkew: make(txSkewResult), - hashEvidence: make(map[string][]hashEvidenceEntry), + nodeSkew: make(txSkewResult), + hashEvidence: make(map[string][]hashEvidenceEntry), computeInterval: 30 * time.Second, } } @@ -741,12 +741,12 @@ func (s *PacketStore) getNodeClockSkewLocked(pubkey string) *NodeClockSkew { name = e.observerID } observers = append(observers, HashEvidenceObserver{ - ObserverID: e.observerID, - ObserverName: name, - RawSkewSec: e.rawSkew, - CorrectedSkewSec: e.corrected, + ObserverID: e.observerID, + ObserverName: name, + RawSkewSec: e.rawSkew, + CorrectedSkewSec: e.corrected, ObserverOffsetSec: e.offset, - Calibrated: e.calibrated, + Calibrated: e.calibrated, }) corrSkews = append(corrSkews, e.corrected) calSummary.TotalSamples++ diff --git a/cmd/server/clock_skew_issue1285_test.go b/cmd/server/clock_skew_issue1285_test.go index 84448c61..f042fa22 100644 --- a/cmd/server/clock_skew_issue1285_test.go +++ b/cmd/server/clock_skew_issue1285_test.go @@ -18,8 +18,10 @@ import ( ) // Synthesizes the repro from issue #1285: -// 30 healthy adverts (skew ~−20s) + 1 historical advert with an RTC reset -// (advertTS = 2024-06-13, observed today → −705d skew). +// +// 30 healthy adverts (skew ~−20s) + 1 historical advert with an RTC reset +// (advertTS = 2024-06-13, observed today → −705d skew). +// // Returns the populated store. func seedIssue1285Repro(t *testing.T) *PacketStore { t.Helper() @@ -29,8 +31,8 @@ func seedIssue1285Repro(t *testing.T) *PacketStore { const pubkey = "RTCRESET" const skewSec = int64(-20) // node clock is 20s BEHIND wall-clock - baseObs := int64(1779000000) // ~mid-2026 - rtcResetAdv := int64(1718281640) // 2024-06-13 (from the issue repro) + baseObs := int64(1779000000) // ~mid-2026 + rtcResetAdv := int64(1718281640) // 2024-06-13 (from the issue repro) var txs []*StoreTx diff --git a/cmd/server/clock_skew_test.go b/cmd/server/clock_skew_test.go index 8c3048db..e3db2621 100644 --- a/cmd/server/clock_skew_test.go +++ b/cmd/server/clock_skew_test.go @@ -15,17 +15,17 @@ func TestClassifySkew(t *testing.T) { expected SkewSeverity }{ {0, SkewOK}, - {60, SkewOK}, // 1 min - {299, SkewOK}, // just under 5 min - {300, SkewWarning}, // exactly 5 min - {1800, SkewWarning}, // 30 min - {3599, SkewWarning}, // just under 1 hour - {3600, SkewCritical}, // exactly 1 hour - {86400, SkewCritical}, // 1 day - {2592000 - 1, SkewCritical}, // just under 30 days - {2592000, SkewAbsurd}, // exactly 30 days - {86400 * 365 - 1, SkewAbsurd}, // just under 365 days - {86400 * 365, SkewNoClock}, // exactly 365 days + {60, SkewOK}, // 1 min + {299, SkewOK}, // just under 5 min + {300, SkewWarning}, // exactly 5 min + {1800, SkewWarning}, // 30 min + {3599, SkewWarning}, // just under 1 hour + {3600, SkewCritical}, // exactly 1 hour + {86400, SkewCritical}, // 1 day + {2592000 - 1, SkewCritical}, // just under 30 days + {2592000, SkewAbsurd}, // exactly 30 days + {86400*365 - 1, SkewAbsurd}, // just under 365 days + {86400 * 365, SkewNoClock}, // exactly 365 days {86400 * 365 * 10, SkewNoClock}, // 10 years (epoch-0 style) } for _, tc := range tests { @@ -228,7 +228,7 @@ func TestComputeNodeSkew_ThreeObservers(t *testing.T) { // raw from obs1 = 60, corrected = 60 + 0 = 60 // raw from obs2 = 60, corrected = 60 + 0 = 60 // raw from obs3 = 30, corrected = 30 + 30 = 60 - // All three converge to 60. + // All three converge to 60. if cs.MedianSkewSec != 60 { t.Errorf("median skew = %v, want 60 (node is 60s ahead)", cs.MedianSkewSec) } diff --git a/cmd/server/config_knobs_test.go b/cmd/server/config_knobs_test.go index 2d3b2b7a..aacd5983 100644 --- a/cmd/server/config_knobs_test.go +++ b/cmd/server/config_knobs_test.go @@ -67,4 +67,3 @@ func TestGraphPruneOlderThan(t *testing.T) { t.Errorf("wrong edge survived prune: %+v", edges[0]) } } - diff --git a/cmd/server/coverage_test.go b/cmd/server/coverage_test.go index cfb28c5d..de4ba76d 100644 --- a/cmd/server/coverage_test.go +++ b/cmd/server/coverage_test.go @@ -4748,4 +4748,3 @@ func TestIngestTraceBroadcastIncludesPath(t *testing.T) { t.Errorf("expected hopsCompleted=2, got %d", *pathStruct.HopsCompleted) } } - diff --git a/cmd/server/decoder.go b/cmd/server/decoder.go index 21fd7eff..8f7981ef 100644 --- a/cmd/server/decoder.go +++ b/cmd/server/decoder.go @@ -85,40 +85,40 @@ type AdvertFlags struct { // Payload is a generic decoded payload. Fields are populated depending on type. type Payload struct { - Type string `json:"type"` - DestHash string `json:"destHash,omitempty"` - SrcHash string `json:"srcHash,omitempty"` - MAC string `json:"mac,omitempty"` - EncryptedData string `json:"encryptedData,omitempty"` - ExtraHash string `json:"extraHash,omitempty"` + Type string `json:"type"` + DestHash string `json:"destHash,omitempty"` + SrcHash string `json:"srcHash,omitempty"` + MAC string `json:"mac,omitempty"` + EncryptedData string `json:"encryptedData,omitempty"` + ExtraHash string `json:"extraHash,omitempty"` // Extended ACK fields per firmware 1.16.0 (issue #1610) — populated by // decodeAck once the server-side re-decoder is upgraded (issue #1694). - AckLen *int `json:"ackLen,omitempty"` - AckAttempt *int `json:"ackAttempt,omitempty"` - AckRand *int `json:"ackRand,omitempty"` - PubKey string `json:"pubKey,omitempty"` - Timestamp uint32 `json:"timestamp,omitempty"` - TimestampISO string `json:"timestampISO,omitempty"` - Signature string `json:"signature,omitempty"` - SignatureValid *bool `json:"signatureValid,omitempty"` - Flags *AdvertFlags `json:"flags,omitempty"` - Lat *float64 `json:"lat,omitempty"` - Lon *float64 `json:"lon,omitempty"` - Name string `json:"name,omitempty"` - ChannelHash int `json:"channelHash,omitempty"` + AckLen *int `json:"ackLen,omitempty"` + AckAttempt *int `json:"ackAttempt,omitempty"` + AckRand *int `json:"ackRand,omitempty"` + PubKey string `json:"pubKey,omitempty"` + Timestamp uint32 `json:"timestamp,omitempty"` + TimestampISO string `json:"timestampISO,omitempty"` + Signature string `json:"signature,omitempty"` + SignatureValid *bool `json:"signatureValid,omitempty"` + Flags *AdvertFlags `json:"flags,omitempty"` + Lat *float64 `json:"lat,omitempty"` + Lon *float64 `json:"lon,omitempty"` + Name string `json:"name,omitempty"` + ChannelHash int `json:"channelHash,omitempty"` // ANON_REQ carries the sender's FULL 32-byte public key (not a 1-byte // srcHash like REQ) — MeshCore firmware/src/Mesh.cpp. Surfaced as // srcPubKey so store.go's node indexer picks it up and it resolves to a // node name; frontend also reads legacy ephemeralPubKey for pre-rename // packets (#1864). - SrcPubKey string `json:"srcPubKey,omitempty"` - PathData string `json:"pathData,omitempty"` - Tag uint32 `json:"tag,omitempty"` - AuthCode uint32 `json:"authCode,omitempty"` - TraceFlags *int `json:"traceFlags,omitempty"` - SNRValues []float64 `json:"snrValues,omitempty"` - RawHex string `json:"raw,omitempty"` - Error string `json:"error,omitempty"` + SrcPubKey string `json:"srcPubKey,omitempty"` + PathData string `json:"pathData,omitempty"` + Tag uint32 `json:"tag,omitempty"` + AuthCode uint32 `json:"authCode,omitempty"` + TraceFlags *int `json:"traceFlags,omitempty"` + SNRValues []float64 `json:"snrValues,omitempty"` + RawHex string `json:"raw,omitempty"` + Error string `json:"error,omitempty"` // GRP_TXT/GRP_DATA channel envelope helpers — see // firmware/src/helpers/BaseChatMesh.cpp:376-391. ChannelHashHex string `json:"channelHashHex,omitempty"` @@ -139,7 +139,7 @@ type Payload struct { InnerAckLen *int `json:"innerAckLen,omitempty"` InnerAckAttempt *int `json:"innerAckAttempt,omitempty"` InnerAckRand *int `json:"innerAckRand,omitempty"` - InnerPayload string `json:"innerPayload,omitempty"` + InnerPayload string `json:"innerPayload,omitempty"` // CONTROL (PAYLOAD_TYPE_CONTROL=0x0B) byte0 flags, per // firmware/src/Mesh.cpp:69 — high-bit = zero-hop direct subset. CtrlFlags string `json:"ctrlFlags,omitempty"` @@ -336,10 +336,10 @@ func decodeAdvert(buf []byte, validateSignatures bool) Payload { off += 8 } if hasFeat1 && len(appdata) >= off+2 { - off += 2 // skip feat1 bytes (reserved for future use) + off += 2 // skip feat1 bytes (reserved for future use) } if hasFeat2 && len(appdata) >= off+2 { - off += 2 // skip feat2 bytes (reserved for future use) + off += 2 // skip feat2 bytes (reserved for future use) } if p.Flags.HasName { name := string(appdata[off:]) @@ -471,11 +471,11 @@ func decodeAnonReq(buf []byte) Payload { return Payload{Type: "ANON_REQ", Error: "too short", RawHex: hex.EncodeToString(buf)} } return Payload{ - Type: "ANON_REQ", - DestHash: hex.EncodeToString(buf[0:1]), - SrcPubKey: hex.EncodeToString(buf[1:33]), - MAC: hex.EncodeToString(buf[33:35]), - EncryptedData: hex.EncodeToString(buf[35:]), + Type: "ANON_REQ", + DestHash: hex.EncodeToString(buf[0:1]), + SrcPubKey: hex.EncodeToString(buf[1:33]), + MAC: hex.EncodeToString(buf[33:35]), + EncryptedData: hex.EncodeToString(buf[35:]), } } @@ -806,7 +806,9 @@ func sanitizeName(s string) string { // advertRole returns a stable role label for an advert. Follows firmware // ADV_TYPE_* constants in firmware/src/helpers/AdvertDataHelpers.h:7-12: -// 0 NONE, 1 CHAT, 2 REPEATER, 3 ROOM, 4 SENSOR, 5-15 FUTURE. +// +// 0 NONE, 1 CHAT, 2 REPEATER, 3 ROOM, 4 SENSOR, 5-15 FUTURE. +// // Previously this coerced both 0 (NONE) and 5-15 (FUTURE) to "companion", // silently relabelling unknown/reserved types — see issue #1279 P1 #3. func advertRole(f *AdvertFlags) string { diff --git a/cmd/server/decoder_test.go b/cmd/server/decoder_test.go index 5d96deac..bdfcdedb 100644 --- a/cmd/server/decoder_test.go +++ b/cmd/server/decoder_test.go @@ -97,7 +97,6 @@ func TestDecodePacket_FloodHasNoCodes(t *testing.T) { } } - func TestZeroHopDirectHashSize(t *testing.T) { // DIRECT (RouteType=2) + REQ (PayloadType=0) → header byte = 0x02 // pathByte=0x00 → hash_count=0, hash_size bits=0 → should get HashSize=0 diff --git a/cmd/server/discovered_channels.go b/cmd/server/discovered_channels.go index 0461de54..84dd1bcb 100644 --- a/cmd/server/discovered_channels.go +++ b/cmd/server/discovered_channels.go @@ -19,10 +19,10 @@ var hashtagRE = regexp.MustCompile(`#[A-Za-z0-9_\-]+`) // preserved so callers can match against canonical channel names directly. // // Examples: -// extractHashtagsFromText("hi #mesh and #fun") => []string{"#mesh", "#fun"} -// extractHashtagsFromText("nothing here") => nil -// extractHashtagsFromText("dup #x and #x again") => []string{"#x"} // +// extractHashtagsFromText("hi #mesh and #fun") => []string{"#mesh", "#fun"} +// extractHashtagsFromText("nothing here") => nil +// extractHashtagsFromText("dup #x and #x again") => []string{"#x"} func extractHashtagsFromText(text string) []string { if text == "" { return nil diff --git a/cmd/server/eviction_test.go b/cmd/server/eviction_test.go index fa79e6b5..e83aa0dd 100644 --- a/cmd/server/eviction_test.go +++ b/cmd/server/eviction_test.go @@ -253,26 +253,26 @@ func TestEvictStale_CleansResolvedPathNodeIndexes(t *testing.T) { defer db.Close() store := &PacketStore{ - packets: make([]*StoreTx, 0), - byHash: make(map[string]*StoreTx), - byTxID: make(map[int]*StoreTx), - byObsID: make(map[int]*StoreObs), - byObserver: make(map[string][]*StoreObs), - byNode: make(map[string][]*StoreTx), - nodeHashes: make(map[string]map[string]bool), - byPayloadType: make(map[int][]*StoreTx), - spIndex: make(map[string]int), - distHops: make([]distHopRecord, 0), - distPaths: make([]distPathRecord, 0), - rfCache: make(map[string]*cachedResult), - topoCache: make(map[string]*cachedResult), - hashCache: make(map[string]*cachedResult), - chanCache: make(map[string]*cachedResult), - distCache: make(map[string]*cachedResult), - subpathCache: make(map[string]*cachedResult), - rfCacheTTL: 15 * time.Second, - retentionHours: 24, - db: db, + packets: make([]*StoreTx, 0), + byHash: make(map[string]*StoreTx), + byTxID: make(map[int]*StoreTx), + byObsID: make(map[int]*StoreObs), + byObserver: make(map[string][]*StoreObs), + byNode: make(map[string][]*StoreTx), + nodeHashes: make(map[string]map[string]bool), + byPayloadType: make(map[int][]*StoreTx), + spIndex: make(map[string]int), + distHops: make([]distHopRecord, 0), + distPaths: make([]distPathRecord, 0), + rfCache: make(map[string]*cachedResult), + topoCache: make(map[string]*cachedResult), + hashCache: make(map[string]*cachedResult), + chanCache: make(map[string]*cachedResult), + distCache: make(map[string]*cachedResult), + subpathCache: make(map[string]*cachedResult), + rfCacheTTL: 15 * time.Second, + retentionHours: 24, + db: db, useResolvedPathIndex: true, } store.initResolvedPathIndex() diff --git a/cmd/server/healthz_test.go b/cmd/server/healthz_test.go index dce8e163..f343b0c7 100644 --- a/cmd/server/healthz_test.go +++ b/cmd/server/healthz_test.go @@ -6,6 +6,7 @@ import ( "net/http/httptest" "testing" ) + func TestHealthzNotReady(t *testing.T) { // Ensure readiness is 0 (not ready) readiness.Store(0) @@ -77,4 +78,3 @@ func TestHealthzAntiTautology(t *testing.T) { t.Fatal("anti-tautology: handler returned 200 when readiness=0; gating is broken") } } - diff --git a/cmd/server/hop_disambig_e2e_test.go b/cmd/server/hop_disambig_e2e_test.go index 6785f665..41ad1f12 100644 --- a/cmd/server/hop_disambig_e2e_test.go +++ b/cmd/server/hop_disambig_e2e_test.go @@ -62,9 +62,9 @@ const ( ) type t1201Node struct { - pk string - lat, lon float64 - obsCount int + pk string + lat, lon float64 + obsCount int } func t1201InsertNode(t *testing.T, db *DB, n t1201Node) { diff --git a/cmd/server/index_ready_1008.go b/cmd/server/index_ready_1008.go index 70256c9e..64517c9d 100644 --- a/cmd/server/index_ready_1008.go +++ b/cmd/server/index_ready_1008.go @@ -55,18 +55,18 @@ // the not-ready window these report zero counts / nil scores // rather than 503-ing the whole list. Acceptable because: // -// * /api/nodes and /api/nodes/{pubkey} have many other fields -// (last-seen, position, advert metadata) that callers depend -// on at startup. 503-ing the SPA bootstrap to wait for an -// index that exclusively affects "relay activity" badges -// would be a worse UX than a 30–60s window of "—" badges. +// - /api/nodes and /api/nodes/{pubkey} have many other fields +// (last-seen, position, advert metadata) that callers depend +// on at startup. 503-ing the SPA bootstrap to wait for an +// index that exclusively affects "relay activity" badges +// would be a worse UX than a 30–60s window of "—" badges. // -// * GetRepeaterRelayInfoMap / GetRepeaterUsefulnessScoreMap / -// GetBridgeScore / repeater_liveness / repeater_usefulness -// all walk s.byPathHop. During the build window they return -// empty maps or zero scores; the steady-state recomputer -// (#1262) refreshes them every 5min once indexes flip ready -// (prewarm guarded by WaitIndexesReady — see review M1). +// - GetRepeaterRelayInfoMap / GetRepeaterUsefulnessScoreMap / +// GetBridgeScore / repeater_liveness / repeater_usefulness +// all walk s.byPathHop. During the build window they return +// empty maps or zero scores; the steady-state recomputer +// (#1262) refreshes them every 5min once indexes flip ready +// (prewarm guarded by WaitIndexesReady — see review M1). // // This is documented rather than gated so operators do not see // /api/nodes 503 during routine restarts on Cascadia-scale data. @@ -215,4 +215,3 @@ func (s *PacketStore) WaitIndexesReady(timeout time.Duration) bool { return s.SubpathIndexReady() && s.PathHopIndexReady() } } - diff --git a/cmd/server/index_ready_1008_test.go b/cmd/server/index_ready_1008_test.go index e8a602ef..f7c6340f 100644 --- a/cmd/server/index_ready_1008_test.go +++ b/cmd/server/index_ready_1008_test.go @@ -2,12 +2,12 @@ // synchronous Load() critical path into a background goroutine. // // Contract: -// 1. Immediately after Load() returns, SubpathIndexReady() and -// PathHopIndexReady() report false (the goroutine has not finished). -// 2. Analytics handlers that depend on those indices respond 503 with -// Retry-After: 5 until the corresponding ready flag flips true. -// 3. After the background build completes (waitable via a helper), -// both flags flip true and handlers respond 200. +// 1. Immediately after Load() returns, SubpathIndexReady() and +// PathHopIndexReady() report false (the goroutine has not finished). +// 2. Analytics handlers that depend on those indices respond 503 with +// Retry-After: 5 until the corresponding ready flag flips true. +// 3. After the background build completes (waitable via a helper), +// both flags flip true and handlers respond 200. package main import ( diff --git a/cmd/server/known_channels_cache.go b/cmd/server/known_channels_cache.go index 3056fe14..5d655e11 100644 --- a/cmd/server/known_channels_cache.go +++ b/cmd/server/known_channels_cache.go @@ -38,7 +38,7 @@ const maxKnownChannelsBytes = 4 * 1024 * 1024 // KnownChannelEntry is one catalogue entry, region-stamped. type KnownChannelEntry struct { - Channel string `json:"channel"` // e.g. "#antwerpen" (# prefix preserved) + Channel string `json:"channel"` // e.g. "#antwerpen" (# prefix preserved) Description string `json:"description,omitempty"` Key string `json:"key,omitempty"` // optional PSK (base64) — present for some entries Region string `json:"region"` // ISO 3166-1 alpha-2 lowercase diff --git a/cmd/server/main.go b/cmd/server/main.go index 88ca4ad9..c4166d1e 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -86,11 +86,11 @@ func main() { } var ( - configDir string - port int - dbPath string - publicDir string - pollMs int + configDir string + port int + dbPath string + publicDir string + pollMs int ) flag.StringVar(&configDir, "config-dir", ".", "Directory containing config.json") diff --git a/cmd/server/memlimit_test.go b/cmd/server/memlimit_test.go index 881816b5..baba03cd 100644 --- a/cmd/server/memlimit_test.go +++ b/cmd/server/memlimit_test.go @@ -61,11 +61,11 @@ func TestMemlimitUnderprovisioned(t *testing.T) { effective, cgroup int64 want bool }{ - {512, 1536, true}, // 512*2=1024 < 1536 → underprovisioned - {768, 1536, false}, // 768*2=1536 == 1536 → not under (boundary) + {512, 1536, true}, // 512*2=1024 < 1536 → underprovisioned + {768, 1536, false}, // 768*2=1536 == 1536 → not under (boundary) {1024, 1536, false}, - {0, 1536, false}, // no effective limit → skip - {512, 0, false}, // no cgroup info → skip + {0, 1536, false}, // no effective limit → skip + {512, 0, false}, // no cgroup info → skip } for _, c := range cases { got := memlimitUnderprovisioned(c.effective, c.cgroup) diff --git a/cmd/server/memory.go b/cmd/server/memory.go index 0d5b0274..2ffe4d7c 100644 --- a/cmd/server/memory.go +++ b/cmd/server/memory.go @@ -13,20 +13,20 @@ import ( // // Field invariants (typical, not guaranteed under exotic conditions): // -// processRSSMB >= goSysMB >= goHeapInuseMB >= storeDataMB +// processRSSMB >= goSysMB >= goHeapInuseMB >= storeDataMB // -// - processRSSMB is what the kernel charges the process (resident set). -// Read from /proc/self/status `VmRSS:` on Linux; falls back to goSysMB -// on other platforms or when /proc is unavailable. -// - goSysMB is the total memory obtained from the OS by the Go runtime -// (heap, stacks, GC metadata, mspans, mcache, etc.). Includes -// fragmentation and unused-but-mapped span overhead. -// - goHeapInuseMB is the live, in-use Go heap (HeapInuse). Excludes -// idle spans and runtime overhead. -// - storeDataMB is the in-store packet byte estimate (transmissions + -// observations). Subset of HeapInuse. Does not include index maps, -// analytics caches, broadcast queues, or runtime overhead. Used as -// the input to the eviction watermark. +// - processRSSMB is what the kernel charges the process (resident set). +// Read from /proc/self/status `VmRSS:` on Linux; falls back to goSysMB +// on other platforms or when /proc is unavailable. +// - goSysMB is the total memory obtained from the OS by the Go runtime +// (heap, stacks, GC metadata, mspans, mcache, etc.). Includes +// fragmentation and unused-but-mapped span overhead. +// - goHeapInuseMB is the live, in-use Go heap (HeapInuse). Excludes +// idle spans and runtime overhead. +// - storeDataMB is the in-store packet byte estimate (transmissions + +// observations). Subset of HeapInuse. Does not include index maps, +// analytics caches, broadcast queues, or runtime overhead. Used as +// the input to the eviction watermark. // // processRSSMB and storeDataMB are monotonic only relative to ingest + // eviction; both can shrink when packets age out. goHeapInuseMB and goSysMB diff --git a/cmd/server/multibyte_capability_test.go b/cmd/server/multibyte_capability_test.go index dedacb2d..1325510a 100644 --- a/cmd/server/multibyte_capability_test.go +++ b/cmd/server/multibyte_capability_test.go @@ -60,7 +60,7 @@ func addTestPacket(store *PacketStore, tx *StoreTx) { // buildPathByte returns a 2-char hex string for the path byte with given // hashSize (1-3) and hopCount. func buildPathByte(hashSize, hopCount int) string { - b := byte(((hashSize - 1) & 0x3) << 6) | byte(hopCount&0x3F) + b := byte(((hashSize-1)&0x3)<<6) | byte(hopCount&0x3F) return fmt.Sprintf("%02x", b) } diff --git a/cmd/server/neighbor_api.go b/cmd/server/neighbor_api.go index 157433ba..ebdcc572 100644 --- a/cmd/server/neighbor_api.go +++ b/cmd/server/neighbor_api.go @@ -15,36 +15,36 @@ import ( // ─── Neighbor API response types ─────────────────────────────────────────────── type NeighborResponse struct { - Node string `json:"node"` - Neighbors []NeighborEntry `json:"neighbors"` - TotalObservations int `json:"total_observations"` + Node string `json:"node"` + Neighbors []NeighborEntry `json:"neighbors"` + TotalObservations int `json:"total_observations"` } type NeighborEntry struct { - Pubkey *string `json:"pubkey"` - Prefix string `json:"prefix"` - Name *string `json:"name"` - Role *string `json:"role"` - Count int `json:"count"` + Pubkey *string `json:"pubkey"` + Prefix string `json:"prefix"` + Name *string `json:"name"` + Role *string `json:"role"` + Count int `json:"count"` // CountsByMode breaks Count down by observation hash-prefix mode in bytes // (1, 2, 4, 6). Lets the frontend weight confidence by ambiguity rather // than treating every sighting as equal evidence. Issue #1638. - CountsByMode map[int]int `json:"counts_by_mode,omitempty"` - Score float64 `json:"score"` - FirstSeen string `json:"first_seen"` - LastSeen string `json:"last_seen"` - AvgSNR *float64 `json:"avg_snr"` - DistanceKm *float64 `json:"distance_km,omitempty"` - Observers []string `json:"observers"` - Ambiguous bool `json:"ambiguous"` - Unresolved bool `json:"unresolved,omitempty"` - Candidates []CandidateEntry `json:"candidates,omitempty"` + CountsByMode map[int]int `json:"counts_by_mode,omitempty"` + Score float64 `json:"score"` + FirstSeen string `json:"first_seen"` + LastSeen string `json:"last_seen"` + AvgSNR *float64 `json:"avg_snr"` + DistanceKm *float64 `json:"distance_km,omitempty"` + Observers []string `json:"observers"` + Ambiguous bool `json:"ambiguous"` + Unresolved bool `json:"unresolved,omitempty"` + Candidates []CandidateEntry `json:"candidates,omitempty"` } type CandidateEntry struct { - Pubkey string `json:"pubkey"` - Name string `json:"name"` - Role string `json:"role"` + Pubkey string `json:"pubkey"` + Name string `json:"name"` + Role string `json:"role"` } type NeighborGraphResponse struct { diff --git a/cmd/server/neighbor_api_test.go b/cmd/server/neighbor_api_test.go index a34d1220..4a146eaf 100644 --- a/cmd/server/neighbor_api_test.go +++ b/cmd/server/neighbor_api_test.go @@ -229,7 +229,7 @@ func TestNeighborAPI_MinCountFilter(t *testing.T) { func TestNeighborAPI_MinScoreFilter(t *testing.T) { now := time.Now() - e1 := newEdge("aaaa", "bbbb", "bb", 100, now) // score ~1.0 + e1 := newEdge("aaaa", "bbbb", "bb", 100, now) // score ~1.0 e2 := newEdge("aaaa", "cccc", "cc", 1, now.Add(-30*24*time.Hour)) // very low score srv := makeTestServer(makeTestGraph(e1, e2)) diff --git a/cmd/server/neighbor_debug.go b/cmd/server/neighbor_debug.go index bf006a89..4d172bea 100644 --- a/cmd/server/neighbor_debug.go +++ b/cmd/server/neighbor_debug.go @@ -38,19 +38,19 @@ type DebugEdge struct { } type DebugResolution struct { - Prefix string `json:"prefix"` - Chosen string `json:"chosen,omitempty"` - ChosenName string `json:"chosenName,omitempty"` - ChosenScore int `json:"chosenScore"` - ChosenJaccard float64 `json:"chosenJaccard"` - Confidence string `json:"confidence"` - Candidates []DebugCandidate `json:"candidates"` - Ratio float64 `json:"ratio"` - ThresholdApplied float64 `json:"thresholdApplied"` - Method string `json:"method"` - Tier string `json:"tier"` - KnownNode string `json:"knownNode"` - KnownNodeName string `json:"knownNodeName,omitempty"` + Prefix string `json:"prefix"` + Chosen string `json:"chosen,omitempty"` + ChosenName string `json:"chosenName,omitempty"` + ChosenScore int `json:"chosenScore"` + ChosenJaccard float64 `json:"chosenJaccard"` + Confidence string `json:"confidence"` + Candidates []DebugCandidate `json:"candidates"` + Ratio float64 `json:"ratio"` + ThresholdApplied float64 `json:"thresholdApplied"` + Method string `json:"method"` + Tier string `json:"tier"` + KnownNode string `json:"knownNode"` + KnownNodeName string `json:"knownNodeName,omitempty"` } type DebugCandidate struct { @@ -61,15 +61,15 @@ type DebugCandidate struct { } type DebugStats struct { - TotalEdges int `json:"totalEdges"` - TotalNodes int `json:"totalNodes"` - ResolvedCount int `json:"resolvedCount"` - AmbiguousCount int `json:"ambiguousCount"` - UnresolvedCount int `json:"unresolvedCount"` - AvgConfidence float64 `json:"avgConfidence"` + TotalEdges int `json:"totalEdges"` + TotalNodes int `json:"totalNodes"` + ResolvedCount int `json:"resolvedCount"` + AmbiguousCount int `json:"ambiguousCount"` + UnresolvedCount int `json:"unresolvedCount"` + AvgConfidence float64 `json:"avgConfidence"` ColdStartCoverage float64 `json:"coldStartCoverage"` - CacheAge string `json:"cacheAge"` - LastRebuild string `json:"lastRebuild"` + CacheAge string `json:"cacheAge"` + LastRebuild string `json:"lastRebuild"` } // ─── Debug API Handler ───────────────────────────────────────────────────────── diff --git a/cmd/server/neighbor_dedup_test.go b/cmd/server/neighbor_dedup_test.go index ca37a8a7..5ab9a050 100644 --- a/cmd/server/neighbor_dedup_test.go +++ b/cmd/server/neighbor_dedup_test.go @@ -226,9 +226,9 @@ func TestPhase1EdgeCollection_Unchanged(t *testing.T) { Timestamp: ts, }} tx := &StoreTx{ - ID: 1, - PayloadType: &payloadType, - DecodedJSON: `{"pubKey":"aaaa1111"}`, + ID: 1, + PayloadType: &payloadType, + DecodedJSON: `{"pubKey":"aaaa1111"}`, Observations: obs, } @@ -430,10 +430,10 @@ func BenchmarkResolveAmbiguousEdges_500(b *testing.B) { pk = hexPK(i) nodes = append(nodes, nodeInfo{ PublicKey: pk, - Name: pk[:8], - HasGPS: true, - Lat: 45.0 + float64(i)*0.01, - Lon: -122.0 + float64(i)*0.01, + Name: pk[:8], + HasGPS: true, + Lat: 45.0 + float64(i)*0.01, + Lon: -122.0 + float64(i)*0.01, }) } pm := buildPrefixMap(nodes) diff --git a/cmd/server/neighbor_graph.go b/cmd/server/neighbor_graph.go index 209c7e67..19ecc63b 100644 --- a/cmd/server/neighbor_graph.go +++ b/cmd/server/neighbor_graph.go @@ -52,18 +52,18 @@ func makeEdgeKey(a, b string) edgeKey { // NeighborEdge represents a weighted, undirected first-hop neighbor relationship. type NeighborEdge struct { - NodeA string // full pubkey - NodeB string // full pubkey, or "" if unresolved/ambiguous - Prefix string // raw hop prefix that established this edge - Count int // total observations - FirstSeen time.Time // - LastSeen time.Time // - SNRSum float64 // running sum for average - SNRCount int // how many SNR samples - Observers map[string]bool // observer pubkeys that witnessed - Ambiguous bool // multiple candidates or zero candidates - Candidates []string // candidate pubkeys when ambiguous - Resolved bool // true if auto-resolved via Jaccard + NodeA string // full pubkey + NodeB string // full pubkey, or "" if unresolved/ambiguous + Prefix string // raw hop prefix that established this edge + Count int // total observations + FirstSeen time.Time // + LastSeen time.Time // + SNRSum float64 // running sum for average + SNRCount int // how many SNR samples + Observers map[string]bool // observer pubkeys that witnessed + Ambiguous bool // multiple candidates or zero candidates + Candidates []string // candidate pubkeys when ambiguous + Resolved bool // true if auto-resolved via Jaccard // CountsByMode tallies sightings broken down by hash-prefix mode in bytes // (1, 2, or 3). Firmware path-byte encoding (Packet.cpp:13-18) sets // hash_size = (pathByte>>6)+1 with values 1/2/3 valid and 4 reserved. @@ -780,7 +780,6 @@ func parseTimestamp(s string) time.Time { return time.Time{} } - // minLen returns the smaller of n and len(s). func minLen(s string, n int) int { if len(s) < n { diff --git a/cmd/server/node_blacklist_test.go b/cmd/server/node_blacklist_test.go index 2f941703..a6d87769 100644 --- a/cmd/server/node_blacklist_test.go +++ b/cmd/server/node_blacklist_test.go @@ -15,13 +15,13 @@ func TestConfigIsBlacklisted(t *testing.T) { } tests := []struct { - pubkey string - want bool + pubkey string + want bool }{ {"AA", true}, - {"aa", true}, // case-insensitive + {"aa", true}, // case-insensitive {"BB", true}, - {"CC", true}, // lowercase "cc" matches uppercase + {"CC", true}, // lowercase "cc" matches uppercase {"DD", false}, {"", false}, {"AAB", false}, diff --git a/cmd/server/nodes_perf_test.go b/cmd/server/nodes_perf_test.go index a1f8921e..c7e1c572 100644 --- a/cmd/server/nodes_perf_test.go +++ b/cmd/server/nodes_perf_test.go @@ -14,8 +14,9 @@ import ( // path-hop activity, and a non-trivial byPayloadType + byPathHop index. // // Regression guard for issue #1257: -// /api/nodes (no limit) → 32.9s, 30KB on staging (637 nodes) -// /api/nodes?limit=2000 → 4.9s, 360KB +// +// /api/nodes (no limit) → 32.9s, 30KB on staging (637 nodes) +// /api/nodes?limit=2000 → 4.9s, 360KB // // Root cause class: per-repeater enrichment in handleNodes calls // store.GetRepeaterRelayInfo + GetRepeaterUsefulnessScore separately for diff --git a/cmd/server/observer_analytics_test.go b/cmd/server/observer_analytics_test.go index ead61da1..602bf81e 100644 --- a/cmd/server/observer_analytics_test.go +++ b/cmd/server/observer_analytics_test.go @@ -205,7 +205,7 @@ func TestBuildNodesTimelineDistinct(t *testing.T) { } base := time.Date(2026, 3, 4, 12, 0, 0, 0, time.UTC) filtered := []*StoreObs{ - buildObsForTest(1, base, nil, `["bb","cc"]`), // bucket A: nodes {aaaa, bb, cc} + buildObsForTest(1, base, nil, `["bb","cc"]`), // bucket A: nodes {aaaa, bb, cc} buildObsForTest(1, base.Add(10*time.Minute), nil, `["bb"]`), // same bucket: dedup } got := buildNodesTimeline(filtered, 1, store.byTxID) diff --git a/cmd/server/observer_blacklist_test.go b/cmd/server/observer_blacklist_test.go index 32ccd71c..ec6b2b47 100644 --- a/cmd/server/observer_blacklist_test.go +++ b/cmd/server/observer_blacklist_test.go @@ -17,9 +17,9 @@ func TestConfigIsObserverBlacklisted(t *testing.T) { want bool }{ {"OBS1", true}, - {"obs1", true}, // case-insensitive + {"obs1", true}, // case-insensitive {"OBS2", true}, - {"Obs3", true}, // whitespace trimmed + {"Obs3", true}, // whitespace trimmed {"obs4", false}, {"", false}, } diff --git a/cmd/server/observers_canrelay_tristate_1290_test.go b/cmd/server/observers_canrelay_tristate_1290_test.go index aa40da97..146f7ae9 100644 --- a/cmd/server/observers_canrelay_tristate_1290_test.go +++ b/cmd/server/observers_canrelay_tristate_1290_test.go @@ -12,9 +12,9 @@ import ( // Issue #1290 (MAJOR-2, adversarial review of PR #1624) — tri-state badge. // // The badge surface needs to distinguish three states: -// 1. legacy observer (never sent `repeat` field) → unknown → no badge -// 2. firmware confirmed `repeat:on` → "Repeater" -// 3. firmware confirmed `repeat:off` → "Listener" +// 1. legacy observer (never sent `repeat` field) → unknown → no badge +// 2. firmware confirmed `repeat:on` → "Repeater" +// 3. firmware confirmed `repeat:off` → "Listener" // // Previously `CanRelay bool` defaulted to false in Go even when the row // was the legacy DEFAULT 1, conflating "confirmed repeater" with diff --git a/cmd/server/openapi_completeness_test.go b/cmd/server/openapi_completeness_test.go index 20cd844e..45a00c4b 100644 --- a/cmd/server/openapi_completeness_test.go +++ b/cmd/server/openapi_completeness_test.go @@ -205,4 +205,3 @@ func TestOpenAPICompleteness(t *testing.T) { len(stale), knownGapsFile, strings.Join(stale, "\n - ")) } } - diff --git a/cmd/server/openapi_test.go b/cmd/server/openapi_test.go index a9fa6a0d..03590530 100644 --- a/cmd/server/openapi_test.go +++ b/cmd/server/openapi_test.go @@ -138,5 +138,3 @@ func TestExtractPathParams(t *testing.T) { } } } - - diff --git a/cmd/server/path_inspect.go b/cmd/server/path_inspect.go index 28843d27..a7c99ed5 100644 --- a/cmd/server/path_inspect.go +++ b/cmd/server/path_inspect.go @@ -18,9 +18,9 @@ import ( // pathInspectRequest is the JSON body for the inspect endpoint. type pathInspectRequest struct { - Prefixes []string `json:"prefixes"` - Context *pathInspectContext `json:"context,omitempty"` - Limit int `json:"limit,omitempty"` + Prefixes []string `json:"prefixes"` + Context *pathInspectContext `json:"context,omitempty"` + Limit int `json:"limit,omitempty"` } type pathInspectContext struct { @@ -31,14 +31,14 @@ type pathInspectContext struct { // pathCandidate is one scored candidate path in the response. type pathCandidate struct { - Path []string `json:"path"` - Names []string `json:"names"` - Score float64 `json:"score"` + Path []string `json:"path"` + Names []string `json:"names"` + Score float64 `json:"score"` // Speculative is true when the score is below speculativeThreshold or any // hop is below the configured path-trust threshold. Consumers needing the // reason should inspect evidence.perHop[].trusted. - Speculative bool `json:"speculative"` - Evidence pathEvidence `json:"evidence"` + Speculative bool `json:"speculative"` + Evidence pathEvidence `json:"evidence"` } type pathEvidence struct { @@ -57,8 +57,8 @@ type hopEvidence struct { // hopAlternative shows a candidate that was considered but not chosen for this hop. type hopAlternative struct { PublicKey string `json:"publicKey"` - Name string `json:"name"` - Score float64 `json:"score"` + Name string `json:"name"` + Score float64 `json:"score"` } type pathInspectResponse struct { @@ -79,15 +79,15 @@ type beamEntry struct { } const ( - beamWidth = 20 - maxInputHops = 64 - maxPrefixBytes = 3 - maxRequestItems = 64 - geoMaxKm = 50.0 - hopScoreFloor = 0.05 + beamWidth = 20 + maxInputHops = 64 + maxPrefixBytes = 3 + maxRequestItems = 64 + geoMaxKm = 50.0 + hopScoreFloor = 0.05 speculativeThreshold = 0.7 - inspectCacheTTL = 30 * time.Second - inspectBodyLimit = 4096 + inspectCacheTTL = 30 * time.Second + inspectBodyLimit = 4096 ) // Weights per spec §2.3. @@ -355,8 +355,8 @@ func (s *PacketStore) beamSearch(prefixes []string, pm *prefixMap, graph *Neighb } newEntry := beamEntry{ - pubkeys: append(append([]string{}, entry.pubkeys...), cand.PublicKey), - names: append(append([]string{}, entry.names...), cand.Name), + pubkeys: append(append([]string{}, entry.pubkeys...), cand.PublicKey), + names: append(append([]string{}, entry.names...), cand.Name), evidence: append(append([]hopEvidence{}, entry.evidence...), hopEvidence{ Prefix: prefix, CandidatesConsidered: candidateCount, @@ -434,7 +434,6 @@ func (s *PacketStore) scoreHop(entry beamEntry, cand nodeInfo, candidateCount in return wEdge*edgeScore + wGeo*geoScore + wRecency*recencyScore + wSelectivity*selectivityScore } - func sortBeam(beam []beamEntry) { sort.Slice(beam, func(i, j int) bool { return beam[i].score > beam[j].score diff --git a/cmd/server/paths_hop_name_test.go b/cmd/server/paths_hop_name_test.go index a3e6a5ed..cee2fd06 100644 --- a/cmd/server/paths_hop_name_test.go +++ b/cmd/server/paths_hop_name_test.go @@ -25,7 +25,7 @@ func TestHandleNodePaths_HopName_CanonicalPathShowsTarget_1144(t *testing.T) { recent := time.Now().Add(-1 * time.Hour).Format(time.RFC3339) recentEpoch := time.Now().Add(-1 * time.Hour).Unix() - targetPK := "37cf0832aaaabbbb" // no GPS + targetPK := "37cf0832aaaabbbb" // no GPS siblingPK := "37bb000011112222" // has GPS — biased resolver picks this without fix mustExec(t, db, `INSERT INTO nodes (public_key, name, role, lat, lon, last_seen, first_seen, advert_count) diff --git a/cmd/server/perf_io.go b/cmd/server/perf_io.go index c20cdc2c..53415e39 100644 --- a/cmd/server/perf_io.go +++ b/cmd/server/perf_io.go @@ -58,7 +58,7 @@ type procIOSample struct { // perfIOTracker keeps the previous sample so handlePerfIO can compute deltas. var ( - perfIOMu sync.Mutex + perfIOMu sync.Mutex perfIOLastSample procIOSample ) @@ -283,17 +283,17 @@ func (s *Server) handlePerfSqlite(w http.ResponseWriter, r *http.Request) { // IngestorStats is the on-disk JSON shape the ingestor writes periodically // for the server to expose via /api/perf/write-sources. type IngestorStats struct { - SampledAt string `json:"sampledAt"` - TxInserted int64 `json:"tx_inserted"` - ObsInserted int64 `json:"obs_inserted"` - DuplicateTx int64 `json:"tx_dupes"` - NodeUpserts int64 `json:"node_upserts"` - ObserverUpserts int64 `json:"observer_upserts"` - WriteErrors int64 `json:"write_errors"` - SignatureDrops int64 `json:"sig_drops"` - WALCommits int64 `json:"walCommits"` - GroupCommitFlushes int64 `json:"groupCommitFlushes"` - BackfillUpdates map[string]int64 `json:"backfillUpdates"` + SampledAt string `json:"sampledAt"` + TxInserted int64 `json:"tx_inserted"` + ObsInserted int64 `json:"obs_inserted"` + DuplicateTx int64 `json:"tx_dupes"` + NodeUpserts int64 `json:"node_upserts"` + ObserverUpserts int64 `json:"observer_upserts"` + WriteErrors int64 `json:"write_errors"` + SignatureDrops int64 `json:"sig_drops"` + WALCommits int64 `json:"walCommits"` + GroupCommitFlushes int64 `json:"groupCommitFlushes"` + BackfillUpdates map[string]int64 `json:"backfillUpdates"` // ProcIO is the ingestor's own /proc/self/io rates (since its previous // sample). Optional — older ingestor builds don't publish this. See #1120. ProcIO *PerfIOSample `json:"procIO,omitempty"` @@ -467,14 +467,14 @@ func (s *Server) handlePerfWriteSources(w http.ResponseWriter, r *http.Request) return } sources := map[string]int64{ - "tx_inserted": st.TxInserted, - "tx_dupes": st.DuplicateTx, - "obs_inserted": st.ObsInserted, - "node_upserts": st.NodeUpserts, - "observer_upserts": st.ObserverUpserts, - "write_errors": st.WriteErrors, - "sig_drops": st.SignatureDrops, - "walCommits": st.WALCommits, + "tx_inserted": st.TxInserted, + "tx_dupes": st.DuplicateTx, + "obs_inserted": st.ObsInserted, + "node_upserts": st.NodeUpserts, + "observer_upserts": st.ObserverUpserts, + "write_errors": st.WriteErrors, + "sig_drops": st.SignatureDrops, + "walCommits": st.WALCommits, "groupCommitFlushes": st.GroupCommitFlushes, } for name, v := range st.BackfillUpdates { diff --git a/cmd/server/perf_io_bench_test.go b/cmd/server/perf_io_bench_test.go index ceeea04a..147925e2 100644 --- a/cmd/server/perf_io_bench_test.go +++ b/cmd/server/perf_io_bench_test.go @@ -31,7 +31,6 @@ func TestPerfIOBench_Sanity(t *testing.T) { } } - // BenchmarkParseProcIOInto measures the server-side /proc/self/io key:value // walker on a representative payload. Carmack must-fix #3. func BenchmarkParseProcIOInto(b *testing.B) { diff --git a/cmd/server/perf_io_carmack_test.go b/cmd/server/perf_io_carmack_test.go index eef0507b..dc80c42f 100644 --- a/cmd/server/perf_io_carmack_test.go +++ b/cmd/server/perf_io_carmack_test.go @@ -138,4 +138,3 @@ func TestReadIngestorIOSample_CacheInvalidatesOnMtimeChange(t *testing.T) { // StartStatsFileWriter and asserts byte-equal sampledAt strings on a // published stats file. Removed per Kent Beck Gate review // pullrequestreview-4254521304. - diff --git a/cmd/server/release_fast_path_workflow_test.go b/cmd/server/release_fast_path_workflow_test.go index 09ff6ad7..4aa87074 100644 --- a/cmd/server/release_fast_path_workflow_test.go +++ b/cmd/server/release_fast_path_workflow_test.go @@ -4,11 +4,11 @@ // files as text and asserting structural invariants. They follow the same // "config gate" pattern as openapi_completeness_test.go. // -// 1. .github/workflows/release-fast-path.yml MUST exist and own the -// push.tags trigger for v-tags, with the two execution branches -// (re-tag-via-crane on SHA match, fallback to deploy.yml otherwise). -// 2. .github/workflows/deploy.yml MUST NOT trigger on push.tags any -// more — the fast-path workflow owns tag pushes to avoid double-fire. +// 1. .github/workflows/release-fast-path.yml MUST exist and own the +// push.tags trigger for v-tags, with the two execution branches +// (re-tag-via-crane on SHA match, fallback to deploy.yml otherwise). +// 2. .github/workflows/deploy.yml MUST NOT trigger on push.tags any +// more — the fast-path workflow owns tag pushes to avoid double-fire. package main import ( @@ -51,12 +51,12 @@ func TestReleaseFastPathWorkflowExists(t *testing.T) { // - re-tag path: install crane, read :edge revision label, apply new tags // - fallback path: dispatch the existing deploy.yml pipeline required := []string{ - "imjasonh/setup-crane", // crane install action - "org.opencontainers.image.revision", // label inspected on :edge - "ghcr.io/kpa-clawbot/corescope", // image ref - ":edge", // source tag we copy from - "crane tag", // metadata-only retag - "workflow run deploy.yml", // fallback dispatch + "imjasonh/setup-crane", // crane install action + "org.opencontainers.image.revision", // label inspected on :edge + "ghcr.io/kpa-clawbot/corescope", // image ref + ":edge", // source tag we copy from + "crane tag", // metadata-only retag + "workflow run deploy.yml", // fallback dispatch } for _, need := range required { if !strings.Contains(src, need) { diff --git a/cmd/server/repeater_usefulness.go b/cmd/server/repeater_usefulness.go index a7476bb7..49b8f86c 100644 --- a/cmd/server/repeater_usefulness.go +++ b/cmd/server/repeater_usefulness.go @@ -12,9 +12,12 @@ import ( // coverage, and redundancy axes are deferred to follow-up work). // // Numerator: count of non-advert StoreTx entries indexed under -// pubkey in byPathHop. +// +// pubkey in byPathHop. +// // Denominator: total non-advert StoreTx entries in the store -// (sum of byPayloadType for all keys != payloadTypeAdvert). +// +// (sum of byPayloadType for all keys != payloadTypeAdvert). // // Returns 0 when there is no non-advert traffic, the pubkey is empty, // or the repeater never appears as a relay hop. Scores are clamped to diff --git a/cmd/server/resolve_non_relay_1290_test.go b/cmd/server/resolve_non_relay_1290_test.go index d7d8aac2..5b36913d 100644 --- a/cmd/server/resolve_non_relay_1290_test.go +++ b/cmd/server/resolve_non_relay_1290_test.go @@ -6,9 +6,9 @@ import ( // Issue #1290 — exclude observers that advertised `repeat:off` (listener-only) // from the path-hop disambiguator candidate set. Three cases: -// 1. repeat:off pubkey → NOT a candidate -// 2. repeat:on pubkey → IS a candidate (regression guard) -// 3. legacy / no field → IS a candidate (back-compat preserve current behavior) +// 1. repeat:off pubkey → NOT a candidate +// 2. repeat:on pubkey → IS a candidate (regression guard) +// 3. legacy / no field → IS a candidate (back-compat preserve current behavior) func TestResolveWithContext_ExcludesNonRelayObservers_Issue1290(t *testing.T) { nodes := []nodeInfo{ {Role: "repeater", PublicKey: "a1aaaaaa", Name: "RealRepeater"}, diff --git a/cmd/server/resolve_with_context_bench_test.go b/cmd/server/resolve_with_context_bench_test.go index 953145be..e88cdc0f 100644 --- a/cmd/server/resolve_with_context_bench_test.go +++ b/cmd/server/resolve_with_context_bench_test.go @@ -120,4 +120,3 @@ func TestResolveWithContextTier1Floor(t *testing.T) { } t.Logf("resolveWithContext tier-1: %v/call (ceiling %v)", perCall, ceiling) } - diff --git a/cmd/server/role_analytics.go b/cmd/server/role_analytics.go index 56a07e0e..8b599983 100644 --- a/cmd/server/role_analytics.go +++ b/cmd/server/role_analytics.go @@ -9,16 +9,16 @@ import ( // RoleStats summarises one role's population and clock-skew posture. type RoleStats struct { - Role string `json:"role"` - NodeCount int `json:"nodeCount"` - WithSkew int `json:"withSkew"` - MeanAbsSkewSec float64 `json:"meanAbsSkewSec"` - MedianAbsSkewSec float64 `json:"medianAbsSkewSec"` - OkCount int `json:"okCount"` - WarningCount int `json:"warningCount"` - CriticalCount int `json:"criticalCount"` - AbsurdCount int `json:"absurdCount"` - NoClockCount int `json:"noClockCount"` + Role string `json:"role"` + NodeCount int `json:"nodeCount"` + WithSkew int `json:"withSkew"` + MeanAbsSkewSec float64 `json:"meanAbsSkewSec"` + MedianAbsSkewSec float64 `json:"medianAbsSkewSec"` + OkCount int `json:"okCount"` + WarningCount int `json:"warningCount"` + CriticalCount int `json:"criticalCount"` + AbsurdCount int `json:"absurdCount"` + NoClockCount int `json:"noClockCount"` } // RoleAnalyticsResponse is the payload returned by /api/analytics/roles. diff --git a/cmd/server/runstartup_load_test.go b/cmd/server/runstartup_load_test.go index 6822a57e..fa7f22f7 100644 --- a/cmd/server/runstartup_load_test.go +++ b/cmd/server/runstartup_load_test.go @@ -170,10 +170,10 @@ func TestRunStartupLoad_BgLoaderRunsAfterLoadChunkedSets_OldestLoaded(t *testing }) var ( - mu sync.Mutex - lastChunkAt time.Time - bgEntryAt time.Time - chunkSawOldest string + mu sync.Mutex + lastChunkAt time.Time + bgEntryAt time.Time + chunkSawOldest string ) store.OnChunkLoaded(func(rowsThisChunk, totalRows int) { mu.Lock() diff --git a/cmd/server/store.go b/cmd/server/store.go index ac039f6a..bffaa2f5 100644 --- a/cmd/server/store.go +++ b/cmd/server/store.go @@ -30,13 +30,13 @@ var payloadTypeNames = map[int]string{ // StoreTx is an in-memory transmission with embedded observations. type StoreTx struct { - ID int - RawHex string - Hash string - FirstSeen string - RouteType *int - PayloadType *int - DecodedJSON string + ID int + RawHex string + Hash string + FirstSeen string + RouteType *int + PayloadType *int + DecodedJSON string // ScopeName is the transmission's region scope name (transmissions.scope_name, // #899). nil means the row is not transport-scoped (SQL NULL), or the schema // has no such column (db.hasScopeName=false); a pointer to "" means @@ -46,18 +46,18 @@ type StoreTx struct { Observations []*StoreObs ObservationCount int // Display fields from longest-path observation - ObserverID string - ObserverName string - ObserverIATA string - SNR *float64 - RSSI *float64 - PathJSON string - Direction string - LatestSeen string // max observation timestamp (or FirstSeen if no observations) - UniqueObserverCount int // cached count of distinct observer IDs + ObserverID string + ObserverName string + ObserverIATA string + SNR *float64 + RSSI *float64 + PathJSON string + Direction string + LatestSeen string // max observation timestamp (or FirstSeen if no observations) + UniqueObserverCount int // cached count of distinct observer IDs // Cached parsed fields (set once, read many) parsedPath []string // cached parsePathJSON result - pathParsed bool // whether parsedPath has been set + pathParsed bool // whether parsedPath has been set decodedOnce sync.Once // guards parsedDecoded parsedDecoded map[string]interface{} // cached json.Unmarshal of DecodedJSON // Dedup map: "observerID|pathJSON" → true for O(1) duplicate checks @@ -134,28 +134,28 @@ func (tx *StoreTx) ParsedDecoded() map[string]interface{} { // acquired in the order listed below. Never acquire a higher-numbered lock // while holding a lower-numbered one. // -// 1. mu (sync.RWMutex) — guards the core packet data: packets, -// indexes (byHash, byTxID, byObsID, byObserver, byNode, -// byPathHop, byPayloadType), counters, and loaded flag. +// 1. mu (sync.RWMutex) — guards the core packet data: packets, +// indexes (byHash, byTxID, byObsID, byObserver, byNode, +// byPathHop, byPayloadType), counters, and loaded flag. // -// 2. cacheMu (sync.Mutex) — guards analytics response caches: -// rfCache, topoCache, hashCache, collisionCache, chanCache, -// distCache, subpathCache, and their TTLs/hit counters. -// Also guards rate-limited invalidation state -// (lastInvalidated, pendingInv). +// 2. cacheMu (sync.Mutex) — guards analytics response caches: +// rfCache, topoCache, hashCache, collisionCache, chanCache, +// distCache, subpathCache, and their TTLs/hit counters. +// Also guards rate-limited invalidation state +// (lastInvalidated, pendingInv). // -// 3. channelsCacheMu (sync.Mutex) — guards the short-lived GetChannels -// cache (channelsCacheKey/Exp/Res). +// 3. channelsCacheMu (sync.Mutex) — guards the short-lived GetChannels +// cache (channelsCacheKey/Exp/Res). // -// 4. groupedCacheMu (sync.Mutex) — guards the short-lived -// QueryGroupedPackets cache. +// 4. groupedCacheMu (sync.Mutex) — guards the short-lived +// QueryGroupedPackets cache. // -// 5. regionObsMu (sync.Mutex) — guards the region→observer mapping -// cache (regionObsCache, regionObsCacheTime). +// 5. regionObsMu (sync.Mutex) — guards the region→observer mapping +// cache (regionObsCache, regionObsCacheTime). // -// 6. hashSizeInfoMu (sync.Mutex) — guards the cached hash-size-info -// result (hashSizeInfoCache). Acquired independently or -// under mu (in EvictStale). +// 6. hashSizeInfoMu (sync.Mutex) — guards the cached hash-size-info +// result (hashSizeInfoCache). Acquired independently or +// under mu (in EvictStale). // // Nesting that occurs today: // - IngestNew: mu → cacheMu → channelsCacheMu (1 → 2 → 3, OK) @@ -185,14 +185,14 @@ type PacketStore struct { insertCount int64 queryCount int64 // Response caches (separate mutex to avoid contention with store RWMutex) - cacheMu sync.RWMutex - rfCache map[string]*cachedResult // region → cached RF result - topoCache map[string]*cachedResult // region → cached topology result - hashCache map[string]*cachedResult // region → cached hash-sizes result - collisionCache map[string]*cachedResult // cached hash-collisions result keyed by region ("" = global) - chanCache map[string]*cachedResult // region → cached channels result - distCache map[string]*cachedResult // region → cached distance result - subpathCache map[string]*cachedResult // params → cached subpaths result + cacheMu sync.RWMutex + rfCache map[string]*cachedResult // region → cached RF result + topoCache map[string]*cachedResult // region → cached topology result + hashCache map[string]*cachedResult // region → cached hash-sizes result + collisionCache map[string]*cachedResult // cached hash-collisions result keyed by region ("" = global) + chanCache map[string]*cachedResult // region → cached channels result + distCache map[string]*cachedResult // region → cached distance result + subpathCache map[string]*cachedResult // params → cached subpaths result rfCacheTTL time.Duration collisionCacheTTL time.Duration // Steady-state analytics recomputers (issue #1240). Each holds the @@ -202,18 +202,18 @@ type PacketStore struct { // GetAnalytics* function serves from Load() instead of running the // on-request compute path. Region/window variants still go through // the legacy TTL cache (compute-on-miss). - analyticsRecomputerMu sync.RWMutex - recompTopology *analyticsRecomputer - recompRF *analyticsRecomputer - recompDistance *analyticsRecomputer - recompChannels *analyticsRecomputer - recompHashCollisions *analyticsRecomputer - recompHashSizes *analyticsRecomputer - recompRoles *analyticsRecomputer + analyticsRecomputerMu sync.RWMutex + recompTopology *analyticsRecomputer + recompRF *analyticsRecomputer + recompDistance *analyticsRecomputer + recompChannels *analyticsRecomputer + recompHashCollisions *analyticsRecomputer + recompHashSizes *analyticsRecomputer + recompRoles *analyticsRecomputer recompObserversClockSkew *analyticsRecomputer recompNodesClockSkew *analyticsRecomputer - cacheHits int64 - cacheMisses int64 + cacheHits int64 + cacheMisses int64 // Rate-limited invalidation (fixes #533: caches cleared faster than hit) lastInvalidated time.Time pendingInv *cacheInvalidation // accumulated dirty flags during cooldown @@ -320,12 +320,12 @@ type PacketStore struct { // GetRepeaterUsefulnessScore loop in handleNodes into one O(N) pass // per 15s TTL window — eliminating N RLock acquisitions and N× // timestamp parses of the same byPathHop entries per request. - repeaterEnrichMu sync.Mutex - repeaterRelayCache map[string]RepeaterRelayInfo - repeaterRelayCacheWin float64 - repeaterRelayAt time.Time - repeaterUsefulCache map[string]float64 - repeaterUsefulAt time.Time + repeaterEnrichMu sync.Mutex + repeaterRelayCache map[string]RepeaterRelayInfo + repeaterRelayCacheWin float64 + repeaterRelayAt time.Time + repeaterUsefulCache map[string]float64 + repeaterUsefulAt time.Time // Steady-state recomputer for the two caches above (#1262). When // started, an initial sync compute prewarms the caches so the very @@ -365,16 +365,15 @@ type PacketStore struct { // Updated incrementally during Load/Ingest/Evict — avoids JSON parsing in GetPerfStoreStats. advertPubkeys map[string]int // pubkey → number of advert packets referencing it - // Resolved path membership index: xxhash → []txID (forward) and txID → []hashes (reverse). // Replaces per-StoreTx/StoreObs ResolvedPath []*string field (#800). - resolvedPubkeyIndex map[uint64][]int // hash(pubkey) → []txID - resolvedPubkeyReverse map[int][]uint64 // txID → []hashes indexed under - useResolvedPathIndex bool // feature flag (default true, off path = conservative) - maxResolvedPubkeyIndexEntries int // hard cap for size warning (0 = use default 5M) - apiResolvedPathLRU map[int][]*string // obsID → resolved path (LRU cache for API) - lruOrder []int // FIFO order for LRU eviction - lruMu sync.RWMutex // guards apiResolvedPathLRU + lruOrder + resolvedPubkeyIndex map[uint64][]int // hash(pubkey) → []txID + resolvedPubkeyReverse map[int][]uint64 // txID → []hashes indexed under + useResolvedPathIndex bool // feature flag (default true, off path = conservative) + maxResolvedPubkeyIndexEntries int // hard cap for size warning (0 = use default 5M) + apiResolvedPathLRU map[int][]*string // obsID → resolved path (LRU cache for API) + lruOrder []int // FIFO order for LRU eviction + lruMu sync.RWMutex // guards apiResolvedPathLRU + lruOrder // Persisted neighbor graph for hop resolution at ingest time. // Accessed via atomic.Pointer because async rebuilds (path_inspect.go @@ -389,7 +388,6 @@ type PacketStore struct { rebuildMu sync.Mutex rebuildInFlt chan struct{} // nil when no rebuild is in flight - // Path inspector score cache (issue #944). inspectMu sync.RWMutex inspectCache map[string]*inspectCachedResult @@ -448,12 +446,12 @@ type PacketStore struct { // promote to atomic.Pointer without also auditing the two call // sites in chunked_load.go's RunStartupLoad and store.go's // loadBackgroundChunks + GetPerfStoreStats + BackgroundLoadError. - bgErrMu sync.RWMutex - backgroundLoadErr string + bgErrMu sync.RWMutex + backgroundLoadErr string // loadCoverageRatio: totalLoaded / totalInDB (0.0–1.0). Updated by // loadBackgroundChunks after the chunk walk; surfaced via the typed // perf payload for prod observability. - loadCoverageRatio float64 + loadCoverageRatio float64 // Async hash migration state: set after migrateContentHashesAsync completes. hashMigrationComplete atomic.Bool @@ -472,11 +470,11 @@ type PacketStore struct { chunkCallbacks []func(rowsThisChunk, totalRows int) // Eviction config and stats - retentionHours float64 // 0 = unlimited - maxMemoryMB int // 0 = unlimited (packet store memory budget) - evicted int64 // total packets evicted - trackedBytes int64 // running total of estimated packet store memory - memoryEstimator func() float64 // injectable for tests; nil = use runtime.ReadMemStats (stats only) + retentionHours float64 // 0 = unlimited + maxMemoryMB int // 0 = unlimited (packet store memory budget) + evicted int64 // total packets evicted + trackedBytes int64 // running total of estimated packet store memory + memoryEstimator func() float64 // injectable for tests; nil = use runtime.ReadMemStats (stats only) // Per-store ReadMemStats cache (5s TTL). Fields (not package-level vars) so // that test helpers constructing &PacketStore{...} directly get independent @@ -585,7 +583,7 @@ func dedupeHopsByPair(hops []distHopRecord, limit int) []map[string]interface{} "dist": h.Dist, "type": h.Type, "bestSnr": floatPtrOrNil(pe.agg.maxSNR), "medianSnr": floatPtrOrNil(medianSNR), "obsCount": pe.agg.obsCount, - "hash": h.Hash, "timestamp": h.Timestamp, + "hash": h.Hash, "timestamp": h.Timestamp, }) } return result @@ -653,12 +651,12 @@ func NewPacketStore(db *DB, cfg *PacketStoreConfig, cacheTTLs ...map[string]inte byPayloadType: make(map[int][]*StoreTx), rfCache: make(map[string]*cachedResult), topoCache: make(map[string]*cachedResult), - hashCache: make(map[string]*cachedResult), + hashCache: make(map[string]*cachedResult), collisionCache: make(map[string]*cachedResult), - chanCache: make(map[string]*cachedResult), - distCache: make(map[string]*cachedResult), - subpathCache: make(map[string]*cachedResult), + chanCache: make(map[string]*cachedResult), + distCache: make(map[string]*cachedResult), + subpathCache: make(map[string]*cachedResult), // #1239: 60 seconds by default. rfCacheTTL is the shared TTL for // the RF, topology, distance, hash-sizes, subpath, and channel // analytics caches. Distance analytics IS viewed live during @@ -667,16 +665,16 @@ func NewPacketStore(db *DB, cfg *PacketStoreConfig, cacheTTLs ...map[string]inte // cold-miss churn during heavy ingest without freezing data. // Override via cacheTTL.analyticsRF in config.json (also // propagates to distance / topology / hash-sizes / etc.). - rfCacheTTL: 60 * time.Second, - collisionCacheTTL: 3600 * time.Second, - invCooldown: 300 * time.Second, - spIndex: make(map[string]int, 4096), - spTxIndex: make(map[string][]*StoreTx, 4096), - advertPubkeys: make(map[string]int), - clockSkew: NewClockSkewEngine(), + rfCacheTTL: 60 * time.Second, + collisionCacheTTL: 3600 * time.Second, + invCooldown: 300 * time.Second, + spIndex: make(map[string]int, 4096), + spTxIndex: make(map[string][]*StoreTx, 4096), + advertPubkeys: make(map[string]int), + clockSkew: NewClockSkewEngine(), useResolvedPathIndex: true, - areaNodeCache: make(map[string]map[string]bool), - areaNodeCacheTimes: make(map[string]time.Time), + areaNodeCache: make(map[string]map[string]bool), + areaNodeCacheTimes: make(map[string]time.Time), } ps.initResolvedPathIndex() if cfg != nil { @@ -1748,7 +1746,6 @@ func (s *PacketStore) addToByNode(tx *StoreTx, pubkey string) bool { return isNew } - // trackAdvertPubkey increments the advertPubkeys refcount for ADVERT packets. // Must be called under s.mu write lock. func (s *PacketStore) trackAdvertPubkey(tx *StoreTx) { @@ -2320,18 +2317,18 @@ func (s *PacketStore) GetPerfStoreStatsTyped() PerfPacketStoreStats { } return PerfPacketStoreStats{ - TotalLoaded: totalLoaded, - TotalObservations: totalObs, - Evicted: int(atomic.LoadInt64(&s.evicted)), - Inserts: atomic.LoadInt64(&s.insertCount), - Queries: atomic.LoadInt64(&s.queryCount), - InMemory: totalLoaded, - SqliteOnly: false, - MaxPackets: 2386092, - EstimatedMB: estimatedMB, - TrackedMB: trackedMB, - AvgBytesPerPacket: avgBytesPerPacket, - MaxMB: s.maxMemoryMB, + TotalLoaded: totalLoaded, + TotalObservations: totalObs, + Evicted: int(atomic.LoadInt64(&s.evicted)), + Inserts: atomic.LoadInt64(&s.insertCount), + Queries: atomic.LoadInt64(&s.queryCount), + InMemory: totalLoaded, + SqliteOnly: false, + MaxPackets: 2386092, + EstimatedMB: estimatedMB, + TrackedMB: trackedMB, + AvgBytesPerPacket: avgBytesPerPacket, + MaxMB: s.maxMemoryMB, Indexes: PacketStoreIndexes{ ByHash: hashIdx, ByObserver: observerIdx, @@ -2700,7 +2697,7 @@ func (s *PacketStore) IngestNewFromDB(sinceID, limit int) ([]map[string]interfac newMaxID := sinceID broadcastTxs := make(map[int]*StoreTx) // track new transmissions for broadcast - hasNewNodes := false // track genuinely new node pubkeys + hasNewNodes := false // track genuinely new node pubkeys var broadcastOrder []int // Hoist getCachedNodesAndPM() once before the observation loop to avoid @@ -2957,7 +2954,7 @@ func (s *PacketStore) IngestNewFromDB(sinceID, limit int) ([]map[string]interfac s.invalidateCachesFor(inv) } - // Per #1287 (Option 4): the server NEVER writes to the DB and + // Per #1287 (Option 4): the server NEVER writes to the DB and // NEVER mutates the in-memory neighbor graph incrementally. The // ingestor owns neighbor_edges; recompNeighborGraph re-reads the // snapshot every 60s and atomic-swaps it into s.graph. We also no @@ -8360,14 +8357,14 @@ func (s *PacketStore) GetAnalyticsHashCollisions(region, area string) map[string // collisionNode is a lightweight node representation for collision analysis. type collisionNode struct { - PublicKey string `json:"public_key"` - Name string `json:"name"` - Role string `json:"role"` - Lat float64 `json:"lat"` - Lon float64 `json:"lon"` - HashSize int `json:"hash_size"` - HashSizeInconsistent bool `json:"hash_size_inconsistent"` - HashSizesSeen []int `json:"hash_sizes_seen,omitempty"` + PublicKey string `json:"public_key"` + Name string `json:"name"` + Role string `json:"role"` + Lat float64 `json:"lat"` + Lon float64 `json:"lon"` + HashSize int `json:"hash_size"` + HashSizeInconsistent bool `json:"hash_size_inconsistent"` + HashSizesSeen []int `json:"hash_sizes_seen,omitempty"` } // collisionEntry represents a prefix collision with pre-computed distances. @@ -8388,10 +8385,10 @@ type prefixCellInfo struct { // twoByteCellInfo holds per-first-byte-group data for 2-byte matrix. type twoByteCellInfo struct { - GroupNodes []collisionNode `json:"group_nodes"` - TwoByteMap map[string][]collisionNode `json:"two_byte_map"` - MaxCollision int `json:"max_collision"` - CollisionCount int `json:"collision_count"` + GroupNodes []collisionNode `json:"group_nodes"` + TwoByteMap map[string][]collisionNode `json:"two_byte_map"` + MaxCollision int `json:"max_collision"` + CollisionCount int `json:"collision_count"` } func (s *PacketStore) computeHashCollisions(region, area string) map[string]interface{} { @@ -9011,8 +9008,8 @@ type MultiByteCapEntry struct { PublicKey string `json:"pubkey"` Name string `json:"name"` Role string `json:"role"` - Status string `json:"status"` // "confirmed", "suspected", "unknown" - Evidence string `json:"evidence"` // "advert", "path", "" + Status string `json:"status"` // "confirmed", "suspected", "unknown" + Evidence string `json:"evidence"` // "advert", "path", "" MaxHashSize int `json:"maxHashSize"` LastSeen string `json:"lastSeen"` } @@ -9020,19 +9017,19 @@ type MultiByteCapEntry struct { // computeMultiByteCapability determines multi-byte capability for each // node (repeaters, companions, rooms, sensors) using two methods: // -// 1. Confirmed: the node has advertised with hash_size >= 2 (from advert -// path byte). This is 100% reliable because the full public key is -// received in adverts — no prefix collision ambiguity. +// 1. Confirmed: the node has advertised with hash_size >= 2 (from advert +// path byte). This is 100% reliable because the full public key is +// received in adverts — no prefix collision ambiguity. // -// 2. Suspected: the node's prefix appears as a hop in a packet whose path -// header indicates hash_size >= 2. This is <100% reliable because -// 2-byte prefixes can collide — two different nodes may share the same -// prefix. If one is confirmed multi-byte and the other is not, the -// non-confirmed one could be a false positive. +// 2. Suspected: the node's prefix appears as a hop in a packet whose path +// header indicates hash_size >= 2. This is <100% reliable because +// 2-byte prefixes can collide — two different nodes may share the same +// prefix. If one is confirmed multi-byte and the other is not, the +// non-confirmed one could be a false positive. // -// 3. Unknown: node has only been seen with 1-byte adverts and no -// multi-byte path appearances. Could be pre-1.14 firmware or 1.14+ -// with default (1-byte) settings. +// 3. Unknown: node has only been seen with 1-byte adverts and no +// multi-byte path appearances. Could be pre-1.14 firmware or 1.14+ +// with default (1-byte) settings. // // Caller must hold NO locks — this method acquires mu.RLock internally. func (s *PacketStore) computeMultiByteCapability(adopterHashSizes map[string]int) []MultiByteCapEntry { diff --git a/cmd/server/websocket.go b/cmd/server/websocket.go index ba4adc90..f40c2b43 100644 --- a/cmd/server/websocket.go +++ b/cmd/server/websocket.go @@ -75,8 +75,8 @@ func (h *Hub) checkOrigin(r *http.Request) bool { // Client is a single WebSocket connection. type Client struct { - conn *websocket.Conn - send chan []byte + conn *websocket.Conn + send chan []byte closeOnce sync.Once } diff --git a/internal/geofilter/geofilter.go b/internal/geofilter/geofilter.go index c9bb6bf1..98ea35b2 100644 --- a/internal/geofilter/geofilter.go +++ b/internal/geofilter/geofilter.go @@ -69,7 +69,7 @@ func PointInPolygon(lat, lon float64, polygon [][2]float64) bool { func DistToSegmentKm(lat, lon float64, a, b [2]float64) float64 { lat1, lon1 := a[0], a[1] lat2, lon2 := b[0], b[1] - cosLat := math.Cos((lat1+lat2) / 2.0 * math.Pi / 180.0) + cosLat := math.Cos((lat1 + lat2) / 2.0 * math.Pi / 180.0) ax := (lon1 - lon) * 111.0 * cosLat ay := (lat1 - lat) * 111.0 bx := (lon2 - lon) * 111.0 * cosLat diff --git a/internal/lora/toa.go b/internal/lora/toa.go index f76aba38..ba584dc8 100644 --- a/internal/lora/toa.go +++ b/internal/lora/toa.go @@ -60,6 +60,7 @@ func PreambleForSF(sf int) int { // coding_coeff = preset.CR directly (we encode the denominator 5..8 // so coefficient = denominator; Semtech notation uses CR ∈ 1..4 with // coefficient = CR+4, which is the same arithmetic). +// // n_preamble = preamble + 4.25 // ToA = (n_preamble + n_payload) · T_sym // diff --git a/internal/lora/toa_test.go b/internal/lora/toa_test.go index 399c7471..4c7bf966 100644 --- a/internal/lora/toa_test.go +++ b/internal/lora/toa_test.go @@ -8,16 +8,17 @@ import ( // Reference values cross-checked against the closed-form computation // shown in issue #1768 and against RadioLib calculateTimeOnAir(): -// freq=869.6 MHz, BW=62.5 kHz, SF=8, CR=4/5, preamble=32 (SF<=8) -// T_sym = 256/62500 = 4.096 ms -// preamble_symbols = 32 + 4.25 = 36.25 // -// PL=8: num = 64 - 32 + 28 + 16 = 76; den = 4*(8 - 0) = 32 (DE=0 since T_sym<16ms) -// ceil(76/32) = 3 → symbols_payload = 8 + 3*5 = 23 -// total = (36.25 + 23) * 4.096 = 242.688 ms -// PL=16: total = 283.648 ms -// PL=32: total = 365.568 ms -// PL=64: total = 529.408 ms +// freq=869.6 MHz, BW=62.5 kHz, SF=8, CR=4/5, preamble=32 (SF<=8) +// T_sym = 256/62500 = 4.096 ms +// preamble_symbols = 32 + 4.25 = 36.25 +// +// PL=8: num = 64 - 32 + 28 + 16 = 76; den = 4*(8 - 0) = 32 (DE=0 since T_sym<16ms) +// ceil(76/32) = 3 → symbols_payload = 8 + 3*5 = 23 +// total = (36.25 + 23) * 4.096 = 242.688 ms +// PL=16: total = 283.648 ms +// PL=32: total = 365.568 ms +// PL=64: total = 529.408 ms // // These match the table in the issue #1768 body to the microsecond. func TestTimeOnAir_DefaultEUPreset(t *testing.T) { @@ -49,11 +50,12 @@ func TestTimeOnAir_DefaultEUPreset(t *testing.T) { // SF11/BW125 → T_sym = 2048/125000 = 16.384 ms ≥ 16ms → DE = 1; preamble = 16. // PL=8: -// num = 8*8 - 4*11 + 28 + 16 = 64 - 44 + 44 = 64 -// den = 4*(11 - 2*1) = 36; ceil(64/36) = 2 -// symbols_payload = 8 + 2*5 = 18 -// preamble_symbols = 16 + 4.25 = 20.25 -// total = (20.25 + 18) * 16.384 = 38.25 * 16.384 ≈ 626.688 ms +// +// num = 8*8 - 4*11 + 28 + 16 = 64 - 44 + 44 = 64 +// den = 4*(11 - 2*1) = 36; ceil(64/36) = 2 +// symbols_payload = 8 + 2*5 = 18 +// preamble_symbols = 16 + 4.25 = 20.25 +// total = (20.25 + 18) * 16.384 = 38.25 * 16.384 ≈ 626.688 ms func TestTimeOnAir_DERangeSF11(t *testing.T) { preset := Preset{ BWkHz: 125, diff --git a/internal/packetpath/path_test.go b/internal/packetpath/path_test.go index 34a5f96b..c8c842e2 100644 --- a/internal/packetpath/path_test.go +++ b/internal/packetpath/path_test.go @@ -81,7 +81,7 @@ func buildTracePacket() (rawHex string, headerPathHops []string, payloadHops []s buf = append(buf, payloadPathBytes...) rawHex = strings.ToUpper(hex.EncodeToString(buf)) - headerPathHops = []string{"1A", "0F"} // SNR values — NOT route hops + headerPathHops = []string{"1A", "0F"} // SNR values — NOT route hops payloadHops = []string{"AA", "BB", "CC"} // actual route hops from payload return } diff --git a/internal/perfio/perfio.go b/internal/perfio/perfio.go index 1ac62ad2..5234a8a8 100644 --- a/internal/perfio/perfio.go +++ b/internal/perfio/perfio.go @@ -33,11 +33,11 @@ type Sample struct { // these fields plus a sampled-at timestamp; the parser populates Counters // directly so there's exactly ONE implementation of the key:value walker. type Counters struct { - ReadBytes int64 - WriteBytes int64 - CancelledWriteBytes int64 - SyscR int64 - SyscW int64 + ReadBytes int64 + WriteBytes int64 + CancelledWriteBytes int64 + SyscR int64 + SyscW int64 } // ParseProcIO reads /proc/self/io-shaped key:value lines from sc and