From 40f664c5879916442869f40fc475720196f3f9cc Mon Sep 17 00:00:00 2001 From: efiten Date: Thu, 3 Sep 2026 18:52:03 +0200 Subject: [PATCH] chore(#1859): gofmt sweep + gofmt/go vet CI gate (rebase of #1881) (#1941) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rebase of #1881 by @SaarMesh-Bot onto current master. Their three commits are preserved, two of them cherry-picked with authorship intact; the sweep itself had to be regenerated. Opened as a new PR rather than force-pushing their branch. Closes #1881 once merged. Addresses parts 1 and 3 of #1859; part 2 landed as #1937. ## Why regenerated rather than merged The sweep in #1881 was cut on 2026-09-02 07:13 and roughly forty PRs landed after it, so it went `CONFLICTING/DIRTY`. Re-running `gofmt` on current master is cheaper and less error-prone than resolving 72 conflicts that are all whitespace. The drift it fixes also grew in the meantime: 66 files now, against 72 then, but spread differently. ## The three commits 1. **`style(#1859)`** — `gofmt -w` across the 14 modules. 66 files. 2. **`test(#1859)`** — @SaarMesh-Bot's fix for the one `go vet` copylocks finding, `cmd/ingestor/coverage_boost_test.go`: the range variable copied a `Config` embedding `sync.Once`. Cherry-picked unchanged. 3. **`ci(#1859)`** — @SaarMesh-Bot's CI step that fails on gofmt drift or vet findings, plus `.git-blame-ignore-revs`. Cherry-picked with one change, noted in the commit message: the ignore file pointed at `04bc80ee`, the sweep commit on their branch, which does not exist on this base and would make `git blame --ignore-revs-file` error. Repointed at `d3a02599`, the sweep here. ## Verification The claim "formatting only" is checked twice rather than asserted: - Every changed file is byte-identical to `gofmt(previous content)`. 0 of 66 deviate. - With line comments and all whitespace stripped, 0 of 66 files differ, so no code outside comments changed. 14 of the 66 also show doc-comment reflow. Since Go 1.19 `gofmt` re-indents indented comment blocks to tabs and inserts a blank comment line before them; the behavior matrix above `resolveHopWithContext` in `cmd/ingestor/path_resolver.go` is a clear example. That is gofmt's own output, not an edit, but it is worth naming because it makes the diff look larger than "whitespace" suggests. The gate was run locally exactly as the workflow runs it: `gofmt` clean, and `go vet` clean in all 14 modules, including `cmd/ingestor` which is what commit 2 fixes. Suites: `cmd/server` ok (80.7s), `internal/packetpath` ok (2.3s), `cmd/ingestor` passes except `TestWriteStatsAtomic_SymlinkAtDestIsReplaced`, which fails identically on bare master with "A required privilege is not held by the client" (Windows symlink privilege on my host, not code). ## Sequencing This should go last in the queue. The sweep touches 66 files, so merging it before the remaining open Go PRs gives each of them a conflict about nothing but formatting. After it lands the gate is active, and any PR with drift fails CI until it runs `gofmt -w`. Excluded from the sweep: the misnamed `Dockerfile.go`, which is a Dockerfile that gofmt cannot parse (the workflow excludes it too), and `docs/DEPLOYMENT.md`, which a case-insensitive filesystem surfaces as a spurious modification against `docs/deployment.md` and is unrelated. --------- Co-authored-by: SaarMesh-Bot <300107934+SaarMesh-Bot@users.noreply.github.com> Co-authored-by: Claude Opus 5 (1M context) --- .git-blame-ignore-revs | 3 + .github/workflows/deploy.yml | 18 ++ cmd/ingestor/async_migration_test.go | 8 +- cmd/ingestor/coverage_boost_test.go | 3 +- cmd/ingestor/decode_error_log_test.go | 7 +- cmd/ingestor/decoder.go | 163 +++++------ cmd/ingestor/decoder_test.go | 26 +- cmd/ingestor/main_test.go | 14 +- cmd/ingestor/mqtt_watchdog_r1_test.go | 2 +- cmd/ingestor/path_resolver.go | 13 +- cmd/ingestor/source_status.go | 6 +- cmd/ingestor/stats_file_bench_test.go | 1 - cmd/ingestor/stats_file_test.go | 5 +- cmd/server/analytics_recomputer.go | 32 +-- cmd/server/analytics_warmup_1659.go | 42 +-- cmd/server/area_filter_test.go | 38 +-- cmd/server/cache_invalidation_test.go | 2 +- cmd/server/cdn_detection.go | 8 +- cmd/server/channel_analytics_test.go | 44 +-- .../channels_message_order_1366_test.go | 8 +- cmd/server/chunked_load_test.go | 6 +- cmd/server/clock_skew.go | 124 ++++----- cmd/server/clock_skew_issue1285_test.go | 10 +- cmd/server/clock_skew_test.go | 24 +- cmd/server/config_knobs_test.go | 1 - cmd/server/coverage_test.go | 1 - cmd/server/decoder.go | 74 ++--- cmd/server/decoder_test.go | 1 - cmd/server/discovered_channels.go | 6 +- cmd/server/eviction_test.go | 40 +-- cmd/server/healthz_test.go | 2 +- cmd/server/hop_disambig_e2e_test.go | 6 +- cmd/server/index_ready_1008.go | 23 +- cmd/server/index_ready_1008_test.go | 12 +- cmd/server/known_channels_cache.go | 2 +- cmd/server/main.go | 10 +- cmd/server/memlimit_test.go | 8 +- cmd/server/memory.go | 26 +- cmd/server/multibyte_capability_test.go | 2 +- cmd/server/neighbor_api.go | 42 +-- cmd/server/neighbor_api_test.go | 2 +- cmd/server/neighbor_debug.go | 42 +-- cmd/server/neighbor_dedup_test.go | 14 +- cmd/server/neighbor_graph.go | 25 +- cmd/server/node_blacklist_test.go | 8 +- cmd/server/nodes_perf_test.go | 5 +- cmd/server/observer_analytics_test.go | 2 +- cmd/server/observer_blacklist_test.go | 4 +- .../observers_canrelay_tristate_1290_test.go | 6 +- cmd/server/openapi_completeness_test.go | 1 - cmd/server/openapi_test.go | 2 - cmd/server/path_inspect.go | 41 ++- cmd/server/paths_hop_name_test.go | 2 +- cmd/server/perf_io.go | 40 +-- cmd/server/perf_io_bench_test.go | 1 - cmd/server/perf_io_carmack_test.go | 1 - cmd/server/release_fast_path_workflow_test.go | 22 +- cmd/server/repeater_usefulness.go | 7 +- cmd/server/resolve_non_relay_1290_test.go | 6 +- cmd/server/resolve_with_context_bench_test.go | 1 - cmd/server/role_analytics.go | 20 +- cmd/server/runstartup_load_test.go | 8 +- cmd/server/store.go | 255 +++++++++--------- cmd/server/websocket.go | 4 +- internal/geofilter/geofilter.go | 2 +- internal/lora/toa.go | 1 + internal/lora/toa_test.go | 30 ++- internal/packetpath/path_test.go | 2 +- internal/perfio/perfio.go | 10 +- 69 files changed, 727 insertions(+), 700 deletions(-) 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