Files
beacon-server/db/migrations/032_mv_observer_activity.sql
MrAlders0n 7650d9b742 feat(observers): per-observer activity endpoint
GET /observers/{id}/activity returns bucketed observation counts, LoRa
airtime, SNR/RSSI aggregates and a payload-type breakdown for one
observer. Airtime is computed at ingest from the raw frame length and
stored on the observation; intervals of 1h and up are served from an
hourly matview so no request path joins back to packets.
2026-09-10 06:37:45 -04:00

25 lines
1.4 KiB
SQL

-- Hourly per-observer rollup so 7d/30d activity sums a few thousand rows instead of scanning
-- a month of observations. payload_type is a key so the breakdown comes from the same rows.
-- sqlc types the cast aggregates as non-null even though airtime_ms/snr_*/rssi_sum are NULL for
-- empty groups, so readers must gate on the matching *_n counts.
CREATE MATERIALIZED VIEW mv_observer_activity_hourly AS
SELECT
observer_id,
payload_type,
date_trunc('hour', heard_at)::timestamptz AS bucket,
COUNT(*)::bigint AS observations,
SUM(airtime_ms)::real AS airtime_ms,
COUNT(airtime_ms)::bigint AS airtime_n,
SUM(snr) FILTER (WHERE NOT (COALESCE(rssi, 0) = 0 AND COALESCE(snr, 0) = 0))::real AS snr_sum,
COUNT(snr) FILTER (WHERE NOT (COALESCE(rssi, 0) = 0 AND COALESCE(snr, 0) = 0))::bigint AS snr_n,
MIN(snr) FILTER (WHERE NOT (COALESCE(rssi, 0) = 0 AND COALESCE(snr, 0) = 0))::real AS snr_min,
SUM(rssi) FILTER (WHERE NOT (COALESCE(rssi, 0) = 0 AND COALESCE(snr, 0) = 0))::bigint AS rssi_sum,
COUNT(rssi) FILTER (WHERE NOT (COALESCE(rssi, 0) = 0 AND COALESCE(snr, 0) = 0))::bigint AS rssi_n
FROM packet_observations
WHERE heard_at > NOW() - INTERVAL '30 days'
AND payload_type IS NOT NULL
GROUP BY observer_id, payload_type, date_trunc('hour', heard_at);
CREATE UNIQUE INDEX idx_mv_observer_activity_hourly
ON mv_observer_activity_hourly(observer_id, payload_type, bucket);