add radio preset view

move refresh for materialized views into helper and call at boot
This commit is contained in:
Enot (ded) Skelly
2026-06-03 13:46:44 -07:00
parent fa12149151
commit 49558c6dd0
4 changed files with 56 additions and 6 deletions
+16 -6
View File
@@ -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)
}
}
+31
View File
@@ -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);
+3
View File
@@ -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;
+6
View File
@@ -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.