* feat(db): packet type domains, row-cursor indexes and the inverted-heard index
* feat(api): exact page cursors for packets, nodes and observers
* feat(db): normalise packet list filters before choosing a statement
* feat(db): one packets statement per filter shape with an exact row cursor
* fix(db): let the window statements decide since after until
* feat(db): exact keyset cursors for nodes and observers; observers page by last_seen
* feat(api): pageCursor on packets, nodes and observers; it wins over cursor
* test(planguard): packets statements and node/observer keysets leave pending; diff and storediff cases
* chore: bump version to 2.1.0
* test(db): turn off JIT in the keyset cursor tests
* feat(planguard): read sqlc statement text and load a strict plan corpus
* fix(planguard): fail on unextracted statements, keep fragments mode-neutral
* feat(planguard): plan assertions, scoped sort exemptions and narrow retired-index verdicts
* fix(planguard): an errored run always fails
* fix(planguard): keep subplan conds off their parent scan; fail on unanalyzed budgets
* feat(planguard): read-only plan runner, reports and the run command
* fix(planguard): validate identifiers in emitted scripts; prove read-only runs
* feat(planguard): end-state corpus, planted regressions and the generated cached-plan policy
* fix(planguard): derive observer cases from data; mark legacy generic plans
* fix(planguard): bound the dense-region route resolvers; test policy refusals
* fix(planguard): re-measure the dense-region route cases; cover every policy refusal
* feat(planguard): synthetic fixture built through the migrations, checked in CI
* ci(planguard): build the fixture once; guard fixture data for identifiers
* fix(planguard): hash every fixture generator input; harden the fixture guard
* feat(planguard): snapshot diff over every column, named result classes and cursor walks
* fix(planguard): bound the relaxed diff classes; derive the cursor tie from data
* fix(planguard): refuse multi-statement text in the corpus runner
* feat: read-only server mode and an HTTP page diff between two builds
* fix(planguard): refuse escape strings and psql meta-commands in corpus text
* fix: storediff follows each build's own cursor; prove read-only gates via the log
* fix(planguard): match the PostgreSQL lexer on high-bit bytes; check every emitted line
* feat(admin): daily read-only plan check on the maintenance pool, its metric and report
* feat(planguard): daily prod check behind planguard.daily_check
* fix(planguard): cancel fresh runs at shutdown; expose a never-ran check
* ci(planguard): pin the fixture server and collation; refresh baselines
* fix(planguard): calibrate route budgets on one dataset; skip entries whose index is absent
* perf(db): drop small-table indexes that block HOT updates
* perf(db): touch known channels by id instead of upserting
* feat: preload channel ids at boot for ingest and maintenance
* feat(db): five pgx pools with fixed session settings and Prometheus tracing
* feat(guards): per-family observe/enforce guards with client slots, deadlines and a shadow budget
* fix(guards): attribute only guard deadlines and classify like the handlers
* feat(db): route API statements by sqlc name; class C always unnamed
* feat(admin): statement statistics by sqlc name; create pg_stat_statements where allowed
* fix(db): skip pg_stat_statements when no schema is creatable
* feat(background): index health against post-rebuild baselines, Postgres activity, job and ingest metrics
* feat: separate API, ingest and maintenance stores; guards on the public API; metrics listener
* fix(db): keep ingest writes waiting, size the ingest pool to the host, tidy
* test(db): give the statements probe a query id of its own
* feat(db): background_state table and the (hop_count, iata) route index
* perf(db): one /routes statement per filter shape, hop pair on custom plans
* feat(db): keyset route retention and grace slices at exact cutoffs
* feat(background): hourly route retention with a bounded grace walk and watermark
* fix(background): bound the grace walk at microsecond cutoffs
* feat(db): stamp-free reconfirm sweep over the route primary key
* feat(background): pace route reconfirm at routes.reconfirm_rows_per_hour, no stamps
* test(routes): drive route retention end to end and tidy comments
* fix(profiling): sample during the hourly route retention run
* perf(routes): keep hop_count indexes out of route search order
* perf(stats): count node types without node_iatas when unfiltered
* perf(db): drop the unused idx_known_routes_iata_last_seen
* fix(db): bound boot migration lock waits and name the blockers
* test(db): pin the lock-retry rebuild and tidy comments
Opening Map and Nodes on a region of a few thousand nodes could spend the
whole 300-request minute and leave the map half loaded. 600 still sits
under the fail2ban flood jail's 900, so the server's 429 answers first.
Top advertisers ranked by flood + direct, so chatty companions with few flood adverts topped the list. sort=flood|direct is now required and applied before the limit; client floors raised to app 1.0.0 / web 2.0.4.
* perf(routes): batch cross-IATA and hash route search (#216)
One search fired a /routes/cross request per IATA pair (138k on 2026-10-08). Route search now takes an optional iatas list and runs a fixed number of index-backed queries. /info advertises built-in minimum client versions.
* chore: bump version to 2.0.4
* chore: add .github/CODEOWNERS for main branch policy
Signed-off-by: MrAlders0n <MrAlders0n@smal.ca>
* chore: add .github/workflows/main-source-guard.yml for main branch policy
Signed-off-by: MrAlders0n <MrAlders0n@smal.ca>
* feat(meshmapper): authenticate API imports (#211)
* feat(observers): add traffic-sorted directory (#212)
* fix(observers): rank available traffic over rolling seven days
* chore: bump version to 2.1.0
* chore: set release version to 2.0.2
* feat(api): add /info with mobile min app version (#214)
* chore: bump version to 2.0.3
---------
Signed-off-by: MrAlders0n <MrAlders0n@smal.ca>
Release the changes on `dev` as v2.0.2: authenticated MeshMapper imports and the traffic-sorted observer directory, including rolling seven-day counts from available history. Bump the API version and regenerate Swagger.
MeshMapper integration requires an API key. Regional admins’ existing per-region or grouped-region coverage keys also work for these APIs. Deployments should set `MESHMAPPER_API_KEY` and restart Beacon.
Validation: `go build ./...`, `gofmt -l .` (empty), `go vet ./...`, `go test ./...`, `govulncheck ./...`, and race tests for MeshMapper and scopestore passed in a clean checkout. PostgreSQL integration tests require CI because Docker is unavailable locally.
* docs: keep only server-specific contributor notes
* docs: trim README to the code and link to beacon-docs
* chore: strip tracking parameters from two airport website URLs
Hourly activity read raw rows only 3h back, so a longer rollup backlog left
hours drawn as zero. The raw tail now starts at the newest complete rollup
hour, capped at the 24h raw holdback, and the response reports rolledUntil
and rawFrom so clients can show anything still uncovered.
The cursor was the last row's last_seen in ms, so routes sharing that
millisecond (common within one upsert batch) never came back. GET /routes
takes an optional cursorId, backed by keyset indexes; cursor alone keeps
working as before.
Lookups turned every error into a 404, and region filters into a 400, so a
transient database failure looked final to clients that don't retry 4xx.
Missing rows stay 404/400; anything else is a 500. Empty /regions and
/iatas lists are [] rather than null.
A full send buffer dropped the oldest event and then the new one too while
reporting one drop. Subscribe accepted routeTypes and observerIds but never
applied them.
GET /traces takes an optional cursorTag so tags sharing a millisecond survive
paging. Migrations run under an advisory lock and record each file in the
same implicit transaction, so a crash can't strand a fresh database.
Overview windowHours counts the hours it covers, hourly observer activity
fills unrolled hours from raw rows, and integration fixtures are UTC.
Record each attempt before sending so a request lost to a deadline still
waits out MeshMapper's limit, and stop holding the directory lock across
HTTP. Groups now qualify only through heard or configured IATAs, and one
missing country list only skips pruning.
A short topic pubkey panicked a lane worker and killed the process; keys must
now be 32 bytes and lane workers recover. Also reject non-finite radio values
before airtime, strip NULs from advert names and neighbour scopes, guard the
MQTT client pointer, and never stream a broker copy of a hearing as a repeat.
Warn at startup when limits are on without trusted_proxies, and once when a
request carries forwarding headers Beacon ignores. Reject short or whitespace
admin keys and negative durations. Key default-window stats by hour, return
500 for scope lookup failures, and validate IATA path params.
Stats endpoints read hourly rollup tables filled once per closed hour by one
advisory-locked task, instead of materialized views that re-scanned every
observation on each refresh. IATA-set tables give exact distinct counts for any
IATA set or region. Cleanup holds raw rows for unrolled hours (up to 24h).
Adds /stats/series; ingest keeps packet observation counts, observer last IATA,
channel message counts and trace tag summaries. 001_baseline.sql is rewritten,
so pre-release 2.0 databases must be recreated. See docs/historical-stats.md.