diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index fc14c65d..b8732e9d 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,5 +1,5 @@ # Default owners for everything -* @446564 +* @MrAlders0n # EXAMPLE Specific ownership by directory diff --git a/README.md b/README.md index 3ecf2d55..cae5c801 100644 --- a/README.md +++ b/README.md @@ -570,4 +570,8 @@ The initial analytics archive migration includes unknown-type activity. Existing development previews that used the earlier draft require a separate operator repair; radio samples already discarded for those legacy rows cannot be recovered. -Saved-route evidence: see [the operator guide](https://github.com/MeshCore-Beacon/beacon-docs/blob/main/app_documentation/saved-route-evidence.md). +### Saved-route evidence + +`GET /api/v1/routes/{iata}/{pathKey}/observations` returns retained observations that match a saved +route's full path bytes, hash size and hop count within its IATA. `range` defaults to `24h` (max `720h`), +`limit` to 50 (max 200); page with `nextPageCursor` → `pageCursor`. Evidence expires with raw packets. diff --git a/db/sqlc/querier.go b/db/sqlc/querier.go index 1f80ab61..ef65fd7b 100644 --- a/db/sqlc/querier.go +++ b/db/sqlc/querier.go @@ -89,8 +89,6 @@ type Querier interface { GetRegionIATAs(ctx context.Context, regionID int32) ([]string, error) GetRouteEvidenceRoute(ctx context.Context, arg GetRouteEvidenceRouteParams) (KnownRoute, error) GetScopeByName(ctx context.Context, name string) (GetScopeByNameRow, error) - // Copyright 2026 Beacon Contributors - // SPDX-License-Identifier: AGPL-3.0-or-later GetScopeCatalogue(ctx context.Context, arg GetScopeCatalogueParams) (MeshmapperScopeCatalogue, error) GetScopeNames(ctx context.Context) ([]string, error) // Aggregate matching observations once, separately from node memberships to avoid diff --git a/db/sqlc/queries.sql.go b/db/sqlc/queries.sql.go index e6fb1edc..3b3def70 100644 --- a/db/sqlc/queries.sql.go +++ b/db/sqlc/queries.sql.go @@ -1510,7 +1510,6 @@ func (q *Queries) GetScopeByName(ctx context.Context, name string) (GetScopeByNa } const getScopeCatalogue = `-- name: GetScopeCatalogue :one - SELECT iata, url, payload, etag, checked_at, attempted_at, next_attempt, last_error FROM meshmapper_scope_catalogues WHERE iata = $1 AND url = $2 ` @@ -1519,8 +1518,6 @@ type GetScopeCatalogueParams struct { Url string `json:"url"` } -// Copyright 2026 Beacon Contributors -// SPDX-License-Identifier: AGPL-3.0-or-later func (q *Queries) GetScopeCatalogue(ctx context.Context, arg GetScopeCatalogueParams) (MeshmapperScopeCatalogue, error) { row := q.db.QueryRow(ctx, getScopeCatalogue, arg.Iata, arg.Url) var i MeshmapperScopeCatalogue diff --git a/internal/ingest/packet.go b/internal/ingest/packet.go index 10222bae..fda54975 100644 --- a/internal/ingest/packet.go +++ b/internal/ingest/packet.go @@ -730,11 +730,7 @@ func (w *Worker) handlePacket(ctx context.Context, iata, pubkeyHex string, raw [ w.log.Error(fmt.Sprintf("db: upsert packet failed from %s/%s", iata, pubkeyHex), "error", err) return } - // Prefer an explicit RFC3339 offset; legacy timezone-less observers use UTC. - heardAt, err := time.Parse(time.RFC3339Nano, envelope.Timestamp) - if err != nil { - heardAt, err = time.Parse("2006-01-02T15:04:05", envelope.Timestamp) - } + heardAt, err := parseHeardAt(envelope.Timestamp) if err != nil { w.log.Warn(fmt.Sprintf("failed to parse timestamp %q", envelope.Timestamp), "error", err) heardAt = time.Now().UTC() @@ -959,3 +955,25 @@ func computeTransportCode(key []byte, payloadType uint8, payload []byte) uint16 } return code } + +// heardAtLayouts lists offset forms first; timezone-less observers are read as UTC. +// Fractional seconds are optional in every layout. +var heardAtLayouts = []string{ + time.RFC3339Nano, + "2006-01-02 15:04:05.999999999Z07:00", + "2006-01-02T15:04:05.999999999Z0700", + "2006-01-02 15:04:05.999999999Z0700", + "2006-01-02T15:04:05", + "2006-01-02 15:04:05", +} + +func parseHeardAt(s string) (time.Time, error) { + var err error + for _, layout := range heardAtLayouts { + var t time.Time + if t, err = time.Parse(layout, s); err == nil { + return t, nil + } + } + return time.Time{}, err +} diff --git a/internal/ingest/timestamp_test.go b/internal/ingest/timestamp_test.go index 1b5daf66..2010f63e 100644 --- a/internal/ingest/timestamp_test.go +++ b/internal/ingest/timestamp_test.go @@ -49,16 +49,36 @@ func TestPacketTimestampFormatsAndSkew(t *testing.T) { } for _, precision := range []time.Duration{time.Second, time.Microsecond, time.Nanosecond} { expected := want.Truncate(precision) - stamp := expected.Format("2006-01-02T15:04:05.999999999") - if got := capture(t, stamp); !got.Equal(expected) { - t.Fatalf("timezone-less UTC changed: got %s want %s", got, expected) + for _, layout := range []string{"2006-01-02T15:04:05.999999999", "2006-01-02 15:04:05.999999999"} { + stamp := expected.Format(layout) + t.Run("naive "+stamp, func(t *testing.T) { + if got := capture(t, stamp); !got.Equal(expected) { + t.Fatalf("timezone-less UTC changed: got %s want %s", got, expected) + } + }) } } + west := time.FixedZone("west", -4*60*60) + for _, layout := range []string{ + "2006-01-02 15:04:05.999999-07:00", // Python str(datetime) + "2006-01-02T15:04:05.999-0700", + "2006-01-02 15:04:05-0700", + } { + stamp := want.Truncate(time.Microsecond).In(west).Format(layout) + expected, _ := time.Parse(layout, stamp) + t.Run(stamp, func(t *testing.T) { + if got := capture(t, stamp); !got.Equal(expected) { + t.Fatalf("reported time changed: got %s want %s", got, expected) + } + }) + } for _, stamp := range []string{"", "invalid", "2026-09-29T25:00:00+00:00", time.Now().Add(31 * time.Minute).Format(time.RFC3339), time.Now().Add(-31 * time.Minute).Format(time.RFC3339)} { - before := time.Now().UTC() - got := capture(t, stamp) - if got.Before(before) || got.After(time.Now().UTC()) { - t.Fatalf("invalid/skewed time escaped server-time guard: %s => %s", stamp, got) - } + t.Run("guard "+stamp, func(t *testing.T) { + before := time.Now().UTC() + got := capture(t, stamp) + if got.Before(before) || got.After(time.Now().UTC()) { + t.Fatalf("invalid/skewed time escaped server-time guard: %s => %s", stamp, got) + } + }) } }