mirror of
https://github.com/MeshCore-Beacon/beacon-server.git
synced 2026-10-17 04:59:55 +00:00
release: v2.0.2
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.
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.31.1
|
||||
// source: observer_directory.sql
|
||||
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
const listObserverDirectory = `-- name: ListObserverDirectory :one
|
||||
|
||||
WITH hours AS MATERIALIZED (
|
||||
SELECT g.h, coalesce(r.status, 'missing') AS status,
|
||||
coalesce(r.status = 'complete', false) AND g.h >= $1::timestamptz
|
||||
AND g.h + interval '1 hour' <= $2::timestamptz AS use_rollup
|
||||
FROM generate_series(date_trunc('hour', $1::timestamptz, 'UTC'), $2::timestamptz, interval '1 hour') g(h)
|
||||
LEFT JOIN analytics_rollup_hours r ON r.hour = g.h
|
||||
WHERE g.h < $2::timestamptz
|
||||
), candidates AS MATERIALIZED (
|
||||
SELECT o.id, o.display_name, o.observer_type,
|
||||
coalesce(o.last_iata, '')::text AS iata,
|
||||
CASE WHEN greatest(coalesce(o.last_status_at, o.last_seen), o.last_seen) > now() - interval '5 minutes'
|
||||
THEN 'online' ELSE 'offline' END AS status,
|
||||
CASE WHEN o.radio_freq_mhz IS NOT NULL AND o.radio_bw_khz IS NOT NULL AND o.radio_sf IS NOT NULL
|
||||
THEN concat(o.radio_freq_mhz, ',', o.radio_bw_khz, ',', o.radio_sf) END AS radio,
|
||||
coalesce((SELECT jsonb_agg(ts.name ORDER BY ts.name) FROM observer_scopes os JOIN transport_scopes ts ON ts.id = os.scope_id WHERE os.observer_id = o.id), '[]'::jsonb) AS scopes
|
||||
FROM observers o
|
||||
WHERE NOT $12::boolean AND (coalesce(cardinality($3::bpchar[]), 0) = 0 OR o.last_iata = ANY($3::bpchar[]))
|
||||
AND ($4::text = '' OR EXISTS (SELECT 1 FROM observer_brokers ob WHERE ob.observer_id = o.id AND ob.broker_name = $4))
|
||||
AND ($5::text = '' OR o.display_name ILIKE '%' || $5 || '%')
|
||||
AND ($6::text = '' OR CASE WHEN greatest(coalesce(o.last_status_at, o.last_seen), o.last_seen) > now() - interval '5 minutes' THEN 'online' ELSE 'offline' END = $6)
|
||||
AND ($7::text = '' OR EXISTS (SELECT 1 FROM observer_scopes os JOIN transport_scopes ts ON ts.id = os.scope_id WHERE os.observer_id = o.id AND ts.name = $7))
|
||||
), raw_traffic AS MATERIALIZED (
|
||||
-- Disjoint hour slices keep raw scans bounded and never recount a completed whole hour.
|
||||
SELECT h.h, r.observer_id, r.observations
|
||||
FROM hours h
|
||||
CROSS JOIN LATERAL (
|
||||
SELECT o.observer_id, count(*)::bigint AS observations
|
||||
FROM packet_observations o
|
||||
WHERE o.heard_at >= greatest(h.h, $1::timestamptz)
|
||||
AND o.heard_at < least(h.h + interval '1 hour', $2::timestamptz)
|
||||
AND (coalesce(cardinality($3::bpchar[]), 0) = 0 OR o.iata = ANY($3::bpchar[]))
|
||||
GROUP BY o.observer_id
|
||||
) r
|
||||
JOIN candidates c ON c.id = r.observer_id
|
||||
WHERE NOT h.use_rollup
|
||||
), coverage AS (
|
||||
SELECT count(*)::integer AS expected,
|
||||
count(*) FILTER (WHERE h.use_rollup)::integer AS complete,
|
||||
count(*) FILTER (WHERE NOT h.use_rollup AND (h.status IN ('complete', 'partial') OR r.h IS NOT NULL))::integer AS partial,
|
||||
count(*) FILTER (WHERE NOT h.use_rollup AND h.status = 'missing' AND r.h IS NULL)::integer AS missing
|
||||
FROM hours h LEFT JOIN (SELECT DISTINCT h FROM raw_traffic) r ON r.h = h.h
|
||||
), traffic AS (
|
||||
SELECT observer_id, sum(observations)::bigint AS observations
|
||||
FROM (
|
||||
SELECT a.observer_id, a.observation_count AS observations
|
||||
FROM analytics_hourly_observer_identity a
|
||||
JOIN hours h ON h.h = a.hour AND h.use_rollup
|
||||
JOIN candidates c ON c.id = a.observer_id
|
||||
WHERE (coalesce(cardinality($3::bpchar[]), 0) = 0 OR a.iata = ANY($3::bpchar[]))
|
||||
UNION ALL
|
||||
SELECT observer_id, observations FROM raw_traffic
|
||||
) counts
|
||||
GROUP BY observer_id
|
||||
), listed AS (
|
||||
SELECT c.id, c.display_name, c.observer_type, c.iata, c.status, c.radio, c.scopes, coalesce(t.observations, 0)::bigint AS observations
|
||||
FROM candidates c LEFT JOIN traffic t ON t.observer_id = c.id
|
||||
WHERE $8::text = '' OR c.observer_type = $8
|
||||
), paged AS (
|
||||
SELECT id, display_name, observer_type, iata, status, radio, scopes, observations FROM listed
|
||||
ORDER BY CASE WHEN $9::text = 'traffic' THEN observations END DESC NULLS LAST,
|
||||
lower(coalesce(display_name, '')) COLLATE "C", id
|
||||
LIMIT $11::integer OFFSET $10::bigint
|
||||
), packed AS (
|
||||
SELECT coalesce(jsonb_agg(
|
||||
jsonb_strip_nulls(jsonb_build_object('id', id, 'displayName', display_name, 'observerType', observer_type, 'iata', iata, 'status', status, 'radio', radio, 'scopes', scopes))
|
||||
|| jsonb_build_object('observationCount', observations)
|
||||
ORDER BY CASE WHEN $9::text = 'traffic' THEN observations END DESC NULLS LAST,
|
||||
lower(coalesce(display_name, '')) COLLATE "C", id
|
||||
), '[]'::jsonb) AS items
|
||||
FROM paged
|
||||
), totals AS (
|
||||
SELECT count(*) AS n, max(observations) AS maximum FROM listed
|
||||
)
|
||||
SELECT jsonb_build_object(
|
||||
'items', p.items,
|
||||
'generatedAt', (extract(epoch FROM now())*1000)::bigint,
|
||||
'hasMore', t.n > $10::bigint + $11::integer,
|
||||
'nextCursor', CASE WHEN t.n > $10::bigint + $11::integer THEN $10::bigint + $11::integer END,
|
||||
'windowStart', (extract(epoch FROM $1::timestamptz)*1000)::bigint,
|
||||
'windowEnd', (extract(epoch FROM $2::timestamptz)*1000)::bigint,
|
||||
'sort', $9::text, 'effectiveSort', $9::text,
|
||||
'maxObservationCount', coalesce(t.maximum, 0),
|
||||
'observerTypes', (SELECT coalesce(jsonb_agg(t.observer_type ORDER BY t.observer_type), '[]'::jsonb) FROM (SELECT DISTINCT observer_type FROM candidates WHERE observer_type IS NOT NULL AND observer_type <> '') t),
|
||||
'coverage', jsonb_build_object('status', CASE WHEN v.complete = v.expected THEN 'complete' WHEN v.complete > 0 OR v.partial > 0 THEN 'partial' ELSE 'unavailable' END,
|
||||
'expectedHours', v.expected, 'completeHours', v.complete, 'partialHours', v.partial, 'missingHours', v.missing)
|
||||
)::text AS page
|
||||
FROM packed p CROSS JOIN coverage v CROSS JOIN totals t
|
||||
`
|
||||
|
||||
type ListObserverDirectoryParams struct {
|
||||
Column1 pgtype.Timestamptz `json:"column_1"`
|
||||
Column2 pgtype.Timestamptz `json:"column_2"`
|
||||
Column3 []string `json:"column_3"`
|
||||
Column4 string `json:"column_4"`
|
||||
Column5 string `json:"column_5"`
|
||||
Column6 string `json:"column_6"`
|
||||
Column7 string `json:"column_7"`
|
||||
Column8 string `json:"column_8"`
|
||||
Column9 string `json:"column_9"`
|
||||
Column10 int64 `json:"column_10"`
|
||||
Column11 int32 `json:"column_11"`
|
||||
Column12 bool `json:"column_12"`
|
||||
}
|
||||
|
||||
// Copyright 2026 Beacon Contributors
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
func (q *Queries) ListObserverDirectory(ctx context.Context, arg ListObserverDirectoryParams) (string, error) {
|
||||
row := q.db.QueryRow(ctx, listObserverDirectory,
|
||||
arg.Column1,
|
||||
arg.Column2,
|
||||
arg.Column3,
|
||||
arg.Column4,
|
||||
arg.Column5,
|
||||
arg.Column6,
|
||||
arg.Column7,
|
||||
arg.Column8,
|
||||
arg.Column9,
|
||||
arg.Column10,
|
||||
arg.Column11,
|
||||
arg.Column12,
|
||||
)
|
||||
var page string
|
||||
err := row.Scan(&page)
|
||||
return page, err
|
||||
}
|
||||
Reference in New Issue
Block a user