feat(aprs): add read-only AprsRepo for aprs.me database access

Wire up Microwaveprop.AprsRepo as an optional secondary Ecto repo that
points at the sister aprs.me Postgres for read-only access. This is the
foundation for upcoming 144 MHz calibration work that uses verified APRS
RF hops as ground truth.

* lib/microwaveprop/aprs_repo.ex declares a read_only repo; no schemas,
  no migrations, no writes — Microwaveprop never persists APRS data.
* Application supervisor starts the repo only when configured (dev/test
  via config blocks, prod via APRS_DATABASE_URL). A missing config logs
  a warning and skips startup so the main app still boots.
* config/dev.exs + config/test.exs add local aprsme_dev / aprsme_test
  endpoints. Test pool uses Ecto SQL Sandbox; tests that don't touch
  APRS data don't need to check the connection out.
* config/runtime.exs guards the prod block on APRS_DATABASE_URL —
  unset env logs a warning and leaves the repo unconfigured.
This commit is contained in:
Graham McIntire 2026-05-01 12:25:33 -05:00
parent 1f8765f757
commit 714d1ce432
No known key found for this signature in database
GPG key ID: F4ABF488E6029E59
5 changed files with 88 additions and 5 deletions

View file

@ -3,6 +3,15 @@ import Config
# Do not include metadata nor timestamps in development logs # Do not include metadata nor timestamps in development logs
config :logger, :default_formatter, format: "[$level] $message\n" config :logger, :default_formatter, format: "[$level] $message\n"
# Read-only secondary repo pointing at the local aprs.me dev database.
# See lib/microwaveprop/aprs_repo.ex — used by APRS calibration tooling
# only; the main app boots fine without it.
config :microwaveprop, Microwaveprop.AprsRepo,
username: "postgres",
hostname: "localhost",
database: "aprsme_dev",
pool_size: 2
# Configure your database # Configure your database
config :microwaveprop, Microwaveprop.Repo, config :microwaveprop, Microwaveprop.Repo,
username: "postgres", username: "postgres",

View file

@ -1,5 +1,10 @@
import Config import Config
alias Microwaveprop.Workers.CanadianSoundingFetchWorker
alias Oban.Plugins.Cron
require Logger
# config/runtime.exs is executed for all environments, including # config/runtime.exs is executed for all environments, including
# during releases. It is executed after compilation and before the # during releases. It is executed after compilation and before the
# system starts, so it is typically used to load production configuration # system starts, so it is typically used to load production configuration
@ -16,9 +21,6 @@ import Config
# #
# Alternatively, you can use `mix phx.gen.release` to generate a `bin/server` # Alternatively, you can use `mix phx.gen.release` to generate a `bin/server`
# script that automatically sets the env var above. # script that automatically sets the env var above.
alias Microwaveprop.Workers.CanadianSoundingFetchWorker
alias Oban.Plugins.Cron
if System.get_env("PHX_SERVER") do if System.get_env("PHX_SERVER") do
config :microwaveprop, MicrowavepropWeb.Endpoint, server: true config :microwaveprop, MicrowavepropWeb.Endpoint, server: true
end end
@ -355,6 +357,24 @@ if config_env() == :prod do
# pool_count: 4, # pool_count: 4,
socket_options: maybe_ipv6 socket_options: maybe_ipv6
# Optional secondary repo for read-only access to aprs.me's database.
# Used by APRS-based 144 MHz calibration tooling. When the env var is
# unset we leave the AprsRepo unconfigured; Microwaveprop.Application
# logs a warning and skips starting it, and the rest of the app boots
# normally.
case System.get_env("APRS_DATABASE_URL") do
nil ->
Logger.warning("APRS_DATABASE_URL not set — Microwaveprop.AprsRepo will not start; APRS calibration disabled.")
:ok
aprs_url ->
config :microwaveprop, Microwaveprop.AprsRepo,
url: aprs_url,
pool_size: String.to_integer(System.get_env("APRS_POOL_SIZE") || "2"),
socket_options: maybe_ipv6
end
config :microwaveprop, MicrowavepropWeb.Endpoint, config :microwaveprop, MicrowavepropWeb.Endpoint,
# SSL terminated by Cloudflare tunnel; generated URLs still use https # SSL terminated by Cloudflare tunnel; generated URLs still use https
url: [host: host, port: 443, scheme: "https"], url: [host: host, port: 443, scheme: "https"],

View file

@ -1,5 +1,6 @@
import Config import Config
alias Ecto.Adapters.SQL.Sandbox
alias Microwaveprop.Qrz.Client alias Microwaveprop.Qrz.Client
# Only in tests, remove the complexity from the password hashing algorithm # Only in tests, remove the complexity from the password hashing algorithm
@ -15,6 +16,21 @@ config :microwaveprop, Client,
plug: {Req.Test, Client}, plug: {Req.Test, Client},
retry: false retry: false
# Read-only secondary repo for aprs.me. Configured here so the app boots
# under MIX_ENV=test, but uses the SQL.Sandbox in :manual mode so tests
# that don't touch APRS data don't have to check the DB out. Tests that
# do query the AprsRepo must call `Ecto.Adapters.SQL.Sandbox.checkout/1`
# explicitly.
config :microwaveprop, Microwaveprop.AprsRepo,
username: "postgres",
password: "postgres",
hostname: "localhost",
database: "aprsme_test#{System.get_env("MIX_TEST_PARTITION")}",
pool: Sandbox,
pool_size: 2,
ownership_timeout: 60_000,
timeout: 60_000
config :microwaveprop, Microwaveprop.Geocoder, config :microwaveprop, Microwaveprop.Geocoder,
plug: {Req.Test, Microwaveprop.Geocoder}, plug: {Req.Test, Microwaveprop.Geocoder},
retry: false retry: false
@ -33,7 +49,7 @@ config :microwaveprop, Microwaveprop.Repo,
password: "postgres", password: "postgres",
hostname: "localhost", hostname: "localhost",
database: "microwaveprop_test#{System.get_env("MIX_TEST_PARTITION")}", database: "microwaveprop_test#{System.get_env("MIX_TEST_PARTITION")}",
pool: Ecto.Adapters.SQL.Sandbox, pool: Sandbox,
pool_size: System.schedulers_online() * 2, pool_size: System.schedulers_online() * 2,
ownership_timeout: 60_000, ownership_timeout: 60_000,
timeout: 60_000 timeout: 60_000

View file

@ -17,6 +17,10 @@ defmodule Microwaveprop.Application do
MicrowavepropWeb.Telemetry, MicrowavepropWeb.Telemetry,
Microwaveprop.PromEx, Microwaveprop.PromEx,
Microwaveprop.Repo, Microwaveprop.Repo,
# AprsRepo is optional — only starts when configured with a :url
# (dev/test from config blocks, prod from APRS_DATABASE_URL). Missing
# config logs a warning and skips startup so the app still boots.
aprs_repo_child_spec(),
{Cluster.Supervisor, [topologies, [name: Microwaveprop.ClusterSupervisor]]}, {Cluster.Supervisor, [topologies, [name: Microwaveprop.ClusterSupervisor]]},
{Phoenix.PubSub, name: Microwaveprop.PubSub}, {Phoenix.PubSub, name: Microwaveprop.PubSub},
# Partitioned Task.Supervisor — callers route heavy `async_stream` # Partitioned Task.Supervisor — callers route heavy `async_stream`
@ -43,7 +47,7 @@ defmodule Microwaveprop.Application do
# See https://hexdocs.pm/elixir/Supervisor.html # See https://hexdocs.pm/elixir/Supervisor.html
# for other strategies and supported options # for other strategies and supported options
opts = [strategy: :one_for_one, name: Microwaveprop.Supervisor] opts = [strategy: :one_for_one, name: Microwaveprop.Supervisor]
result = Supervisor.start_link(children, opts) result = children |> Enum.reject(&is_nil/1) |> Supervisor.start_link(opts)
# Attach the Oban exception telemetry tap so per-worker recovery # Attach the Oban exception telemetry tap so per-worker recovery
# callbacks and structured logs fire on job failure. Must run after # callbacks and structured logs fire on job failure. Must run after
@ -92,6 +96,23 @@ defmodule Microwaveprop.Application do
:ok :ok
end end
# Returns Microwaveprop.AprsRepo child spec if configured (has a :url
# or :database key), otherwise logs a warning and returns nil so the
# supervisor children list can filter it out. APRS calibration is an
# optional integration with the sister aprs.me database — we never
# crash boot when it's unavailable.
defp aprs_repo_child_spec do
config = Application.get_env(:microwaveprop, Microwaveprop.AprsRepo, [])
if Keyword.has_key?(config, :url) or Keyword.has_key?(config, :database) do
Microwaveprop.AprsRepo
else
Logger.warning("Microwaveprop.AprsRepo not configured (no :url or :database) — APRS calibration disabled.")
nil
end
end
@doc """ @doc """
Returns the deployment (image build) timestamp. Returns the deployment (image build) timestamp.

View file

@ -0,0 +1,17 @@
defmodule Microwaveprop.AprsRepo do
@moduledoc """
Secondary repo for read-only access to aprs.me's `packets` table.
Used by `Microwaveprop.Aprs` and the `Mix.Tasks.Calibrate.Aprs144` mix
task to pull verified RF hops as ground truth for 144 MHz scoring.
This repo never owns schema, migrations, or writes it only ever
issues SELECT queries against aprs.me's existing partitioned `packets`
table. The connection is configured to fail closed if `APRS_DATABASE_URL`
is unset in production.
"""
use Ecto.Repo,
otp_app: :microwaveprop,
adapter: Ecto.Adapters.Postgres,
read_only: true
end