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
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
config :microwaveprop, Microwaveprop.Repo,
username: "postgres",

View file

@ -1,5 +1,10 @@
import Config
alias Microwaveprop.Workers.CanadianSoundingFetchWorker
alias Oban.Plugins.Cron
require Logger
# config/runtime.exs is executed for all environments, including
# during releases. It is executed after compilation and before the
# 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`
# script that automatically sets the env var above.
alias Microwaveprop.Workers.CanadianSoundingFetchWorker
alias Oban.Plugins.Cron
if System.get_env("PHX_SERVER") do
config :microwaveprop, MicrowavepropWeb.Endpoint, server: true
end
@ -355,6 +357,24 @@ if config_env() == :prod do
# pool_count: 4,
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,
# SSL terminated by Cloudflare tunnel; generated URLs still use https
url: [host: host, port: 443, scheme: "https"],

View file

@ -1,5 +1,6 @@
import Config
alias Ecto.Adapters.SQL.Sandbox
alias Microwaveprop.Qrz.Client
# Only in tests, remove the complexity from the password hashing algorithm
@ -15,6 +16,21 @@ config :microwaveprop, Client,
plug: {Req.Test, Client},
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,
plug: {Req.Test, Microwaveprop.Geocoder},
retry: false
@ -33,7 +49,7 @@ config :microwaveprop, Microwaveprop.Repo,
password: "postgres",
hostname: "localhost",
database: "microwaveprop_test#{System.get_env("MIX_TEST_PARTITION")}",
pool: Ecto.Adapters.SQL.Sandbox,
pool: Sandbox,
pool_size: System.schedulers_online() * 2,
ownership_timeout: 60_000,
timeout: 60_000

View file

@ -17,6 +17,10 @@ defmodule Microwaveprop.Application do
MicrowavepropWeb.Telemetry,
Microwaveprop.PromEx,
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]]},
{Phoenix.PubSub, name: Microwaveprop.PubSub},
# Partitioned Task.Supervisor — callers route heavy `async_stream`
@ -43,7 +47,7 @@ defmodule Microwaveprop.Application do
# See https://hexdocs.pm/elixir/Supervisor.html
# for other strategies and supported options
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
# callbacks and structured logs fire on job failure. Must run after
@ -92,6 +96,23 @@ defmodule Microwaveprop.Application do
:ok
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 """
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