From 49558c6dd099705b4db7ce252d0edc8c6ef36f53 Mon Sep 17 00:00:00 2001 From: "Enot (ded) Skelly" Date: Wed, 3 Jun 2026 13:46:44 -0700 Subject: [PATCH] add radio preset view move refresh for materialized views into helper and call at boot --- cmd/tower/main.go | 22 ++++++++++++++++------ db/migrations/001_schema.sql | 31 +++++++++++++++++++++++++++++++ db/queries/queries.sql | 3 +++ db/store.go | 6 ++++++ 4 files changed, 56 insertions(+), 6 deletions(-) diff --git a/cmd/tower/main.go b/cmd/tower/main.go index 8233a0d..e63cb18 100644 --- a/cmd/tower/main.go +++ b/cmd/tower/main.go @@ -108,6 +108,9 @@ func main() { store := db.New(pool) + // refresh meterialized views on boot or restart to stay fresh + refreshMaterializedViews(ctx, store) + // ── Seed config data ───────────────────────────────────────────────────── if err := config.Seed(ctx, cfg, store); err != nil { log.Fatalf("failed to seed config: %v", err) @@ -194,12 +197,7 @@ func main() { if err := store.DeleteOldPackets(ctx, time.Now().Add(-packetRetention)); err != nil { log.Printf("cleanup: delete old packets failed: %v", err) } - if err := store.RefreshHourlyStats(ctx); err != nil { - log.Printf("cleanup: refresh materialized view for hourly stats failed: %v", err) - } - if err := store.RefreshTopNodes(ctx); err != nil { - log.Printf("cleanup: refresh materialized view for top nodes failed: %v", err) - } + refreshMaterializedViews(ctx, store) case <-ctx.Done(): return } @@ -254,3 +252,15 @@ func mustEnv(key string) string { } return v } + +func refreshMaterializedViews(ctx context.Context, store *db.Store) { + if err := store.RefreshHourlyStats(ctx); err != nil { + log.Printf("refresh: materialized view for hourly stats failed: %v", err) + } + if err := store.RefreshTopNodes(ctx); err != nil { + log.Printf("refresh: materialized view for top nodes failed: %v", err) + } + if err := store.RefreshRadioPresets(ctx); err != nil { + log.Printf("refresh: materialized view for radio presets failed: %v", err) + } +} diff --git a/db/migrations/001_schema.sql b/db/migrations/001_schema.sql index 651297e..dc5f832 100644 --- a/db/migrations/001_schema.sql +++ b/db/migrations/001_schema.sql @@ -340,3 +340,34 @@ CREATE UNIQUE INDEX idx_mv_top_nodes CREATE UNIQUE INDEX idx_mv_hourly_iata_stats ON mv_hourly_iata_stats(iata, hour); + +CREATE MATERIALIZED VIEW mv_radio_presets AS +SELECT + concat(o.radio_freq_mhz, ',', o.radio_bw_khz, ',', o.radio_sf) AS preset, + (SELECT po.iata FROM packet_observations po WHERE po.observer_id = o.id ORDER BY po.heard_at DESC LIMIT 1) AS iata, + 'observer' AS source_type, + COUNT(*) AS count +FROM observers o +WHERE o.radio_freq_mhz IS NOT NULL + AND o.radio_sf IS NOT NULL + AND o.radio_bw_khz IS NOT NULL +GROUP BY concat(o.radio_freq_mhz, ',', o.radio_bw_khz, ',', o.radio_sf), + (SELECT po.iata FROM packet_observations po WHERE po.observer_id = o.id ORDER BY po.heard_at DESC LIMIT 1) +HAVING (SELECT po.iata FROM packet_observations po WHERE po.observer_id = o.id ORDER BY po.heard_at DESC LIMIT 1) IS NOT NULL + +UNION ALL + +SELECT + concat(n.radio_freq_mhz, ',', n.radio_bw_khz, ',', n.radio_sf) AS preset, + ni.iata, + 'node' AS source_type, + COUNT(*) AS count +FROM nodes n +JOIN node_iatas ni ON ni.node_id = n.id +WHERE n.radio_freq_mhz IS NOT NULL + AND n.radio_sf IS NOT NULL + AND n.radio_bw_khz IS NOT NULL +GROUP BY concat(n.radio_freq_mhz, ',', n.radio_bw_khz, ',', n.radio_sf), ni.iata; + +CREATE UNIQUE INDEX idx_mv_radio_presets + ON mv_radio_presets(preset, iata, source_type); diff --git a/db/queries/queries.sql b/db/queries/queries.sql index 6a6257b..ac0a040 100644 --- a/db/queries/queries.sql +++ b/db/queries/queries.sql @@ -636,3 +636,6 @@ REFRESH MATERIALIZED VIEW CONCURRENTLY mv_hourly_iata_stats; -- name: RefreshTopNodes :exec REFRESH MATERIALIZED VIEW CONCURRENTLY mv_top_nodes_by_iata; + +-- name: RefreshRadioPresets :exec +REFRESH MATERIALIZED VIEW CONCURRENTLY mv_radio_presets; diff --git a/db/store.go b/db/store.go index 35e1ffc..031b415 100644 --- a/db/store.go +++ b/db/store.go @@ -759,6 +759,12 @@ func (s *Store) RefreshTopNodes(ctx context.Context) error { return s.q.RefreshTopNodes(ctx) } +// RefreshRadioPresets refreshes the mv_radio_presets materialized view. +// Called by the cleanup goroutine to keep top node rankings current. +func (s *Store) RefreshRadioPresets(ctx context.Context) error { + return s.q.RefreshRadioPresets(ctx) +} + // GetObserverTelemetry returns telemetry points for an observer within the given time range. // since and until define the window; pass zero times to use defaults (last 24h). // TODO: implement server-side bucketing by interval when needed.