diff --git a/config/config.exs b/config/config.exs
index 3886eb39..429a9309 100644
--- a/config/config.exs
+++ b/config/config.exs
@@ -51,7 +51,7 @@ config :microwaveprop, Oban,
{Oban.Plugins.Cron,
crontab: [
{"0 8 * * *", Microwaveprop.Workers.SolarIndexWorker},
- {"*/30 * * * *", Microwaveprop.Workers.QsoWeatherEnqueueWorker},
+ {"*/30 * * * *", Microwaveprop.Workers.ContactWeatherEnqueueWorker},
{"*/5 * * * *", Microwaveprop.Commercial.PollWorker},
{"5 * * * *", Microwaveprop.Workers.PropagationGridWorker}
]}
diff --git a/config/dev.exs b/config/dev.exs
index a2be5edf..e1eff2c0 100644
--- a/config/dev.exs
+++ b/config/dev.exs
@@ -79,7 +79,7 @@ config :microwaveprop, Oban,
crontab: [
{"5 * * * *", Microwaveprop.Workers.PropagationGridWorker},
{"0 8 * * *", Microwaveprop.Workers.SolarIndexWorker},
- {"*/30 * * * *", Microwaveprop.Workers.QsoWeatherEnqueueWorker}
+ {"*/30 * * * *", Microwaveprop.Workers.ContactWeatherEnqueueWorker}
]}
]
diff --git a/lib/microwaveprop/radio.ex b/lib/microwaveprop/radio.ex
index 9ebb312b..bad33f4e 100644
--- a/lib/microwaveprop/radio.ex
+++ b/lib/microwaveprop/radio.ex
@@ -3,21 +3,21 @@ defmodule Microwaveprop.Radio do
import Ecto.Query
+ alias Microwaveprop.Radio.Contact
alias Microwaveprop.Radio.Maidenhead
- alias Microwaveprop.Radio.Qso
alias Microwaveprop.Repo
@per_page 20
@sortable_fields ~w(station1 station2 band mode distance_km qso_timestamp inserted_at)a
- def list_qsos(opts \\ []) do
+ def list_contacts(opts \\ []) do
page = max(Keyword.get(opts, :page, 1), 1)
offset = (page - 1) * @per_page
search = Keyword.get(opts, :search)
{sort_field, sort_dir} = sort_opts(opts)
- base_query = maybe_search(Qso, search)
+ base_query = maybe_search(Contact, search)
total_entries = Repo.aggregate(base_query, :count)
total_pages = max(ceil(total_entries / @per_page), 1)
@@ -39,26 +39,26 @@ defmodule Microwaveprop.Radio do
end
@doc """
- Groups reciprocal QSOs (same station pair swapped, same band, same timestamp).
- Returns a list of `{primary_qso, reciprocals}` tuples where reciprocals is
- a (possibly empty) list of matching QSOs.
+ Groups reciprocal contacts (same station pair swapped, same band, same timestamp).
+ Returns a list of `{primary, reciprocals}` tuples where reciprocals is
+ a (possibly empty) list of matching contacts.
"""
- def group_reciprocals(qsos) do
+ def group_reciprocals(contacts) do
{groups, _seen_ids} =
- Enum.reduce(qsos, {[], MapSet.new()}, fn qso, {groups, seen} ->
- if MapSet.member?(seen, qso.id) do
+ Enum.reduce(contacts, {[], MapSet.new()}, fn contact, {groups, seen} ->
+ if MapSet.member?(seen, contact.id) do
{groups, seen}
else
reciprocals =
- Enum.filter(qsos, fn other ->
- other.id != qso.id and
+ Enum.filter(contacts, fn other ->
+ other.id != contact.id and
not MapSet.member?(seen, other.id) and
- other.band == qso.band and
- reciprocal_pair?(qso, other)
+ other.band == contact.band and
+ reciprocal_pair?(contact, other)
end)
- new_seen = Enum.reduce(reciprocals, MapSet.put(seen, qso.id), &MapSet.put(&2, &1.id))
- {[{qso, reciprocals} | groups], new_seen}
+ new_seen = Enum.reduce(reciprocals, MapSet.put(seen, contact.id), &MapSet.put(&2, &1.id))
+ {[{contact, reciprocals} | groups], new_seen}
end
end)
@@ -97,75 +97,75 @@ defmodule Microwaveprop.Radio do
{field, dir}
end
- def unprocessed_qsos(limit \\ 500) do
- Qso
+ def unprocessed_contacts(limit \\ 500) do
+ Contact
|> where([q], q.weather_queued == false and not is_nil(q.pos1))
|> order_by([q], asc: q.qso_timestamp)
|> limit(^limit)
|> Repo.all()
end
- def mark_weather_queued!(qso_ids) do
- Qso
- |> where([q], q.id in ^qso_ids)
+ def mark_weather_queued!(ids) do
+ Contact
+ |> where([q], q.id in ^ids)
|> Repo.update_all(set: [weather_queued: true])
end
- def unprocessed_hrrr_qsos(limit \\ 500) do
- Qso
+ def unprocessed_hrrr_contacts(limit \\ 500) do
+ Contact
|> where([q], q.hrrr_queued == false and not is_nil(q.pos1))
|> order_by([q], asc: q.qso_timestamp)
|> limit(^limit)
|> Repo.all()
end
- def mark_hrrr_queued!(qso_ids) do
- Qso
- |> where([q], q.id in ^qso_ids)
+ def mark_hrrr_queued!(ids) do
+ Contact
+ |> where([q], q.id in ^ids)
|> Repo.update_all(set: [hrrr_queued: true])
end
- def unprocessed_terrain_qsos(limit \\ 500) do
- Qso
+ def unprocessed_terrain_contacts(limit \\ 500) do
+ Contact
|> where([q], q.terrain_queued == false and not is_nil(q.pos1) and not is_nil(q.pos2))
|> order_by([q], asc: q.qso_timestamp)
|> limit(^limit)
|> Repo.all()
end
- def mark_terrain_queued!(qso_ids) do
- Qso
- |> where([q], q.id in ^qso_ids)
+ def mark_terrain_queued!(ids) do
+ Contact
+ |> where([q], q.id in ^ids)
|> Repo.update_all(set: [terrain_queued: true])
end
- def unprocessed_iemre_qsos(limit \\ 500) do
- Qso
+ def unprocessed_iemre_contacts(limit \\ 500) do
+ Contact
|> where([q], q.iemre_queued == false and not is_nil(q.pos1))
|> order_by([q], asc: q.qso_timestamp)
|> limit(^limit)
|> Repo.all()
end
- def mark_iemre_queued!(qso_ids) do
- Qso
- |> where([q], q.id in ^qso_ids)
+ def mark_iemre_queued!(ids) do
+ Contact
+ |> where([q], q.id in ^ids)
|> Repo.update_all(set: [iemre_queued: true])
end
@doc """
- Returns a list of {lat, lon} points along the QSO path: pos1, midpoint, pos2.
+ Returns a list of {lat, lon} points along the contact path: pos1, midpoint, pos2.
Returns [pos1] if pos2 is nil, or [] if pos1 is nil.
"""
- def qso_path_points(%{pos1: nil}), do: []
+ def contact_path_points(%{pos1: nil}), do: []
- def qso_path_points(%{pos1: pos1, pos2: nil}) do
+ def contact_path_points(%{pos1: pos1, pos2: nil}) do
lat = pos1["lat"]
lon = pos1["lon"] || pos1["lng"]
if lat && lon, do: [{lat, lon}], else: []
end
- def qso_path_points(%{pos1: pos1, pos2: pos2}) do
+ def contact_path_points(%{pos1: pos1, pos2: pos2}) do
lat1 = pos1["lat"]
lon1 = pos1["lon"] || pos1["lng"]
lat2 = pos2["lat"]
@@ -195,19 +195,19 @@ defmodule Microwaveprop.Radio do
2 * @earth_radius_km * :math.asin(:math.sqrt(a))
end
- def backfill_distances(qsos) do
- Enum.each(qsos, fn qso ->
- if is_nil(qso.distance_km) && qso.pos1 && qso.pos2 do
- lat1 = qso.pos1["lat"]
- lon1 = qso.pos1["lon"] || qso.pos1["lng"]
- lat2 = qso.pos2["lat"]
- lon2 = qso.pos2["lon"] || qso.pos2["lng"]
+ def backfill_distances(contacts) do
+ Enum.each(contacts, fn contact ->
+ if is_nil(contact.distance_km) && contact.pos1 && contact.pos2 do
+ lat1 = contact.pos1["lat"]
+ lon1 = contact.pos1["lon"] || contact.pos1["lng"]
+ lat2 = contact.pos2["lat"]
+ lon2 = contact.pos2["lon"] || contact.pos2["lng"]
if lat1 && lon1 && lat2 && lon2 do
dist = lat1 |> haversine_km(lon1, lat2, lon2) |> round() |> Decimal.new()
- Qso
- |> where([q], q.id == ^qso.id)
+ Contact
+ |> where([q], q.id == ^contact.id)
|> Repo.update_all(set: [distance_km: dist])
end
end
@@ -216,16 +216,16 @@ defmodule Microwaveprop.Radio do
defp deg_to_rad(deg), do: deg * :math.pi() / 180
- def get_qso!(id) do
- Repo.get!(Qso, id)
+ def get_contact!(id) do
+ Repo.get!(Contact, id)
end
- def change_qso(qso, attrs \\ %{}) do
- Qso.submission_changeset(qso, attrs)
+ def change_contact(contact, attrs \\ %{}) do
+ Contact.submission_changeset(contact, attrs)
end
- def create_qso(attrs) do
- changeset = Qso.submission_changeset(%Qso{}, attrs)
+ def create_contact(attrs) do
+ changeset = Contact.submission_changeset(%Contact{}, attrs)
if changeset.valid? do
grid1 = Ecto.Changeset.get_change(changeset, :grid1)
diff --git a/lib/microwaveprop/radio/contact.ex b/lib/microwaveprop/radio/contact.ex
index ad9b8d13..9a21712f 100644
--- a/lib/microwaveprop/radio/contact.ex
+++ b/lib/microwaveprop/radio/contact.ex
@@ -1,4 +1,4 @@
-defmodule Microwaveprop.Radio.Qso do
+defmodule Microwaveprop.Radio.Contact do
@moduledoc false
use Ecto.Schema
@@ -33,8 +33,8 @@ defmodule Microwaveprop.Radio.Qso do
@required_fields ~w(station1 station2 qso_timestamp mode band)a
@optional_fields ~w(grid1 grid2 pos1 pos2 distance_km)a
- def changeset(qso, attrs) do
- qso
+ def changeset(contact, attrs) do
+ contact
|> cast(attrs, @required_fields ++ @optional_fields)
|> validate_required(@required_fields)
end
@@ -43,8 +43,8 @@ defmodule Microwaveprop.Radio.Qso do
@allowed_modes ~w(CW SSB FM FT8 FT4)
@allowed_bands Enum.map(~w(1296 2304 3456 5760 10000 24000 47000 75000), &Decimal.new/1)
- def submission_changeset(qso, attrs) do
- qso
+ def submission_changeset(contact, attrs) do
+ contact
|> cast(attrs, @submission_fields)
|> validate_required(@submission_fields)
|> validate_grid(:grid1)
diff --git a/lib/microwaveprop/terrain/terrain_profile.ex b/lib/microwaveprop/terrain/terrain_profile.ex
index b4a71f6d..ccb46aea 100644
--- a/lib/microwaveprop/terrain/terrain_profile.ex
+++ b/lib/microwaveprop/terrain/terrain_profile.ex
@@ -8,7 +8,7 @@ defmodule Microwaveprop.Terrain.TerrainProfile do
@foreign_key_type :binary_id
schema "terrain_profiles" do
- belongs_to :qso, Microwaveprop.Radio.Qso
+ belongs_to :contact, Microwaveprop.Radio.Contact, foreign_key: :qso_id
field :sample_count, :integer
field :path_points, {:array, :map}
field :max_elevation_m, :float
diff --git a/lib/microwaveprop/weather.ex b/lib/microwaveprop/weather.ex
index 05946e12..ceb0177f 100644
--- a/lib/microwaveprop/weather.ex
+++ b/lib/microwaveprop/weather.ex
@@ -126,10 +126,10 @@ defmodule Microwaveprop.Weather do
Enum.uniq(times)
end
- def weather_for_qso(qso_params, opts \\ []) do
- lat = qso_params[:lat] || qso_params.lat
- lon = qso_params[:lon] || qso_params.lon
- timestamp = qso_params[:timestamp] || qso_params.timestamp
+ def weather_for_contact(contact_params, opts \\ []) do
+ lat = contact_params[:lat] || contact_params.lat
+ lon = contact_params[:lon] || contact_params.lon
+ timestamp = contact_params[:timestamp] || contact_params.timestamp
radius_km = Keyword.get(opts, :radius_km, 150)
time_window_hours = Keyword.get(opts, :time_window_hours, 6)
@@ -203,14 +203,14 @@ defmodule Microwaveprop.Weather do
|> Repo.exists?()
end
- def hrrr_for_qso(%{pos1: nil}), do: nil
+ def hrrr_for_contact(%{pos1: nil}), do: nil
- def hrrr_for_qso(qso) do
- lat = qso.pos1["lat"]
- lon = qso.pos1["lon"] || qso.pos1["lng"]
+ def hrrr_for_contact(contact) do
+ lat = contact.pos1["lat"]
+ lon = contact.pos1["lon"] || contact.pos1["lng"]
if lat && lon do
- find_nearest_hrrr(lat, lon, qso.qso_timestamp)
+ find_nearest_hrrr(lat, lon, contact.qso_timestamp)
end
end
@@ -245,10 +245,10 @@ defmodule Microwaveprop.Weather do
def hrrr_profiles_for_path(%{pos1: nil}), do: []
- def hrrr_profiles_for_path(qso) do
- qso
- |> Microwaveprop.Radio.qso_path_points()
- |> Enum.map(fn {lat, lon} -> find_nearest_hrrr(lat, lon, qso.qso_timestamp) end)
+ def hrrr_profiles_for_path(contact) do
+ contact
+ |> Microwaveprop.Radio.contact_path_points()
+ |> Enum.map(fn {lat, lon} -> find_nearest_hrrr(lat, lon, contact.qso_timestamp) end)
|> Enum.reject(&is_nil/1)
end
@@ -301,14 +301,14 @@ defmodule Microwaveprop.Weather do
|> Repo.exists?()
end
- def iemre_for_qso(%{pos1: nil}), do: nil
+ def iemre_for_contact(%{pos1: nil}), do: nil
- def iemre_for_qso(qso) do
- lat = qso.pos1["lat"]
- lon = qso.pos1["lon"] || qso.pos1["lng"]
+ def iemre_for_contact(contact) do
+ lat = contact.pos1["lat"]
+ lon = contact.pos1["lon"] || contact.pos1["lng"]
if lat && lon do
- find_nearest_iemre(lat, lon, qso.qso_timestamp)
+ find_nearest_iemre(lat, lon, contact.qso_timestamp)
end
end
@@ -323,10 +323,10 @@ defmodule Microwaveprop.Weather do
def iemre_for_path(%{pos1: nil}), do: []
- def iemre_for_path(qso) do
- qso
- |> Microwaveprop.Radio.qso_path_points()
- |> Enum.map(fn {lat, lon} -> find_nearest_iemre(lat, lon, qso.qso_timestamp) end)
+ def iemre_for_path(contact) do
+ contact
+ |> Microwaveprop.Radio.contact_path_points()
+ |> Enum.map(fn {lat, lon} -> find_nearest_iemre(lat, lon, contact.qso_timestamp) end)
|> Enum.reject(&is_nil/1)
end
end
diff --git a/lib/microwaveprop/workers/contact_weather_enqueue_worker.ex b/lib/microwaveprop/workers/contact_weather_enqueue_worker.ex
index 09666c96..b9f5635b 100644
--- a/lib/microwaveprop/workers/contact_weather_enqueue_worker.ex
+++ b/lib/microwaveprop/workers/contact_weather_enqueue_worker.ex
@@ -1,4 +1,4 @@
-defmodule Microwaveprop.Workers.QsoWeatherEnqueueWorker do
+defmodule Microwaveprop.Workers.ContactWeatherEnqueueWorker do
@moduledoc false
use Oban.Worker, queue: :enqueue, max_attempts: 3
@@ -16,14 +16,14 @@ defmodule Microwaveprop.Workers.QsoWeatherEnqueueWorker do
@insert_batch_size 400
@doc """
- Enqueue all enrichment jobs (weather, HRRR, terrain, IEMRE) for a single QSO.
+ Enqueue all enrichment jobs (weather, HRRR, terrain, IEMRE) for a single contact.
Called directly from submission flow — no Oban indirection.
"""
- def enqueue_for_qso(qso) do
- weather_jobs = build_weather_jobs([qso])
- hrrr_jobs = build_hrrr_jobs([qso])
- terrain_jobs = build_terrain_jobs([qso])
- iemre_jobs = build_iemre_jobs([qso])
+ def enqueue_for_contact(contact) do
+ weather_jobs = build_weather_jobs([contact])
+ hrrr_jobs = build_hrrr_jobs([contact])
+ terrain_jobs = build_terrain_jobs([contact])
+ iemre_jobs = build_iemre_jobs([contact])
all_jobs = weather_jobs ++ hrrr_jobs ++ terrain_jobs ++ iemre_jobs
@@ -31,11 +31,11 @@ defmodule Microwaveprop.Workers.QsoWeatherEnqueueWorker do
insert_all_chunked(all_jobs)
end
- qso_ids = [qso.id]
- if qso.pos1, do: Radio.mark_weather_queued!(qso_ids)
- if qso.pos1, do: Radio.mark_hrrr_queued!(qso_ids)
- if qso.pos1 && qso.pos2, do: Radio.mark_terrain_queued!(qso_ids)
- if qso.pos1, do: Radio.mark_iemre_queued!(qso_ids)
+ ids = [contact.id]
+ if contact.pos1, do: Radio.mark_weather_queued!(ids)
+ if contact.pos1, do: Radio.mark_hrrr_queued!(ids)
+ if contact.pos1 && contact.pos2, do: Radio.mark_terrain_queued!(ids)
+ if contact.pos1, do: Radio.mark_iemre_queued!(ids)
:ok
end
@@ -51,85 +51,82 @@ defmodule Microwaveprop.Workers.QsoWeatherEnqueueWorker do
end
defp enqueue_weather_jobs do
- case Radio.unprocessed_qsos() do
+ case Radio.unprocessed_contacts() do
[] ->
:ok
- qsos ->
- Radio.backfill_distances(qsos)
+ contacts ->
+ Radio.backfill_distances(contacts)
- jobs = build_weather_jobs(qsos)
+ jobs = build_weather_jobs(contacts)
if jobs != [] do
insert_all_chunked(jobs)
end
- qso_ids = Enum.map(qsos, & &1.id)
- Radio.mark_weather_queued!(qso_ids)
+ ids = Enum.map(contacts, & &1.id)
+ Radio.mark_weather_queued!(ids)
enqueue_weather_jobs()
end
end
defp enqueue_hrrr_jobs do
- case Radio.unprocessed_hrrr_qsos() do
+ case Radio.unprocessed_hrrr_contacts() do
[] ->
:ok
- qsos ->
- jobs = build_hrrr_jobs(qsos)
+ contacts ->
+ jobs = build_hrrr_jobs(contacts)
if jobs != [] do
insert_all_chunked(jobs)
end
- qso_ids = Enum.map(qsos, & &1.id)
- Radio.mark_hrrr_queued!(qso_ids)
+ ids = Enum.map(contacts, & &1.id)
+ Radio.mark_hrrr_queued!(ids)
enqueue_hrrr_jobs()
end
end
defp enqueue_terrain_jobs do
- case Radio.unprocessed_terrain_qsos() do
+ case Radio.unprocessed_terrain_contacts() do
[] ->
:ok
- qsos ->
- jobs = build_terrain_jobs(qsos)
+ contacts ->
+ jobs = build_terrain_jobs(contacts)
if jobs != [] do
insert_all_chunked(jobs)
end
- qso_ids = Enum.map(qsos, & &1.id)
- Radio.mark_terrain_queued!(qso_ids)
+ ids = Enum.map(contacts, & &1.id)
+ Radio.mark_terrain_queued!(ids)
- # Continue until all QSOs are enqueued
enqueue_terrain_jobs()
end
end
- def build_terrain_jobs(qsos) do
- qsos
- |> Enum.filter(fn qso -> qso.pos1 && qso.pos2 end)
- |> Enum.map(fn qso ->
- TerrainProfileWorker.new(%{"qso_id" => qso.id})
+ def build_terrain_jobs(contacts) do
+ contacts
+ |> Enum.filter(fn contact -> contact.pos1 && contact.pos2 end)
+ |> Enum.map(fn contact ->
+ TerrainProfileWorker.new(%{"qso_id" => contact.id})
end)
|> Enum.uniq_by(fn changeset -> changeset.changes.args end)
end
- def build_weather_jobs(qsos) do
- qsos
- |> Enum.flat_map(&jobs_for_qso/1)
+ def build_weather_jobs(contacts) do
+ contacts
+ |> Enum.flat_map(&jobs_for_contact/1)
|> Enum.uniq_by(fn changeset -> changeset.changes.args end)
end
- def build_hrrr_jobs(qsos) do
- # Group all QSO points by HRRR hour, then create one batch job per hour
- # instead of one job per point. This deduplicates GRIB2 downloads.
- qsos
- |> Enum.flat_map(&hrrr_points_for_qso/1)
+ def build_hrrr_jobs(contacts) do
+ contacts
+ |> Enum.flat_map(&hrrr_points_for_contact/1)
|> Enum.group_by(fn {_point, hour} -> hour end, fn {point, _hour} -> point end)
|> Enum.map(fn {hour, points} ->
unique_points =
@@ -144,47 +141,47 @@ defmodule Microwaveprop.Workers.QsoWeatherEnqueueWorker do
end)
end
- def build_iemre_jobs(qsos) do
- qsos
- |> Enum.flat_map(&iemre_job_for_qso/1)
+ def build_iemre_jobs(contacts) do
+ contacts
+ |> Enum.flat_map(&iemre_job_for_contact/1)
|> Enum.uniq_by(fn changeset -> changeset.changes.args end)
end
- defp jobs_for_qso(qso) do
- qso
- |> Radio.qso_path_points()
+ defp jobs_for_contact(contact) do
+ contact
+ |> Radio.contact_path_points()
|> Enum.flat_map(fn {lat, lon} ->
- build_asos_jobs(lat, lon, qso.qso_timestamp) ++
- build_raob_jobs(lat, lon, qso.qso_timestamp)
+ build_asos_jobs(lat, lon, contact.qso_timestamp) ++
+ build_raob_jobs(lat, lon, contact.qso_timestamp)
end)
end
defp enqueue_iemre_jobs do
- case Radio.unprocessed_iemre_qsos() do
+ case Radio.unprocessed_iemre_contacts() do
[] ->
:ok
- qsos ->
- jobs = build_iemre_jobs(qsos)
+ contacts ->
+ jobs = build_iemre_jobs(contacts)
if jobs != [] do
insert_all_chunked(jobs)
end
- qso_ids = Enum.map(qsos, & &1.id)
- Radio.mark_iemre_queued!(qso_ids)
+ ids = Enum.map(contacts, & &1.id)
+ Radio.mark_iemre_queued!(ids)
enqueue_iemre_jobs()
end
end
- defp hrrr_points_for_qso(%{pos1: nil}), do: []
+ defp hrrr_points_for_contact(%{pos1: nil}), do: []
- defp hrrr_points_for_qso(qso) do
- rounded_time = HrrrClient.nearest_hrrr_hour(qso.qso_timestamp)
+ defp hrrr_points_for_contact(contact) do
+ rounded_time = HrrrClient.nearest_hrrr_hour(contact.qso_timestamp)
- qso
- |> Radio.qso_path_points()
+ contact
+ |> Radio.contact_path_points()
|> Enum.flat_map(fn {lat, lon} ->
{rlat, rlon} = Weather.round_to_hrrr_grid(lat, lon)
@@ -216,13 +213,13 @@ defmodule Microwaveprop.Workers.QsoWeatherEnqueueWorker do
end)
end
- defp iemre_job_for_qso(%{pos1: nil}), do: []
+ defp iemre_job_for_contact(%{pos1: nil}), do: []
- defp iemre_job_for_qso(qso) do
- date = DateTime.to_date(qso.qso_timestamp)
+ defp iemre_job_for_contact(contact) do
+ date = DateTime.to_date(contact.qso_timestamp)
- qso
- |> Radio.qso_path_points()
+ contact
+ |> Radio.contact_path_points()
|> Enum.flat_map(fn {lat, lon} ->
{rlat, rlon} = Weather.round_to_iemre_grid(lat, lon)
diff --git a/lib/microwaveprop/workers/terrain_profile_worker.ex b/lib/microwaveprop/workers/terrain_profile_worker.ex
index 7f8eb71e..11ed3c87 100644
--- a/lib/microwaveprop/workers/terrain_profile_worker.ex
+++ b/lib/microwaveprop/workers/terrain_profile_worker.ex
@@ -18,16 +18,16 @@ defmodule Microwaveprop.Workers.TerrainProfileWorker do
if Terrain.has_terrain_profile?(qso_id) do
:ok
else
- qso = Radio.get_qso!(qso_id)
- lat1 = qso.pos1["lat"]
- lon1 = qso.pos1["lon"] || qso.pos1["lng"]
- lat2 = qso.pos2["lat"]
- lon2 = qso.pos2["lon"] || qso.pos2["lng"]
- dist_km = Decimal.to_float(qso.distance_km)
- freq_ghz = Decimal.to_float(qso.band) / 1000
+ contact = Radio.get_contact!(qso_id)
+ lat1 = contact.pos1["lat"]
+ lon1 = contact.pos1["lon"] || contact.pos1["lng"]
+ lat2 = contact.pos2["lat"]
+ lon2 = contact.pos2["lon"] || contact.pos2["lng"]
+ dist_km = Decimal.to_float(contact.distance_km)
+ freq_ghz = Decimal.to_float(contact.band) / 1000
# Look up HRRR refractivity gradient for dynamic k-factor
- k = lookup_k_factor(qso)
+ k = lookup_k_factor(contact)
case ElevationClient.fetch_elevation_profile(lat1, lon1, lat2, lon2) do
{:ok, profile} ->
@@ -64,8 +64,8 @@ defmodule Microwaveprop.Workers.TerrainProfileWorker do
end
end
- defp lookup_k_factor(qso) do
- case Weather.hrrr_for_qso(qso) do
+ defp lookup_k_factor(contact) do
+ case Weather.hrrr_for_contact(contact) do
%{min_refractivity_gradient: grad} when not is_nil(grad) ->
TerrainAnalysis.k_factor(grad)
diff --git a/lib/microwaveprop_web/controllers/page_controller.ex b/lib/microwaveprop_web/controllers/page_controller.ex
index e821961d..726de104 100644
--- a/lib/microwaveprop_web/controllers/page_controller.ex
+++ b/lib/microwaveprop_web/controllers/page_controller.ex
@@ -4,4 +4,12 @@ defmodule MicrowavepropWeb.PageController do
def home(conn, _params) do
redirect(conn, to: ~p"/map")
end
+
+ def redirect_contacts(conn, _params) do
+ redirect(conn, to: ~p"/contacts")
+ end
+
+ def redirect_contact(conn, %{"id" => id}) do
+ redirect(conn, to: ~p"/contacts/#{id}")
+ end
end
diff --git a/lib/microwaveprop_web/live/contact_live/index.ex b/lib/microwaveprop_web/live/contact_live/index.ex
index 995e8781..c3868c93 100644
--- a/lib/microwaveprop_web/live/contact_live/index.ex
+++ b/lib/microwaveprop_web/live/contact_live/index.ex
@@ -1,4 +1,4 @@
-defmodule MicrowavepropWeb.QsoLive.Index do
+defmodule MicrowavepropWeb.ContactLive.Index do
@moduledoc false
use MicrowavepropWeb, :live_view
@@ -21,7 +21,7 @@ defmodule MicrowavepropWeb.QsoLive.Index do
search = Map.get(params, "search", "")
result =
- Radio.list_qsos(
+ Radio.list_contacts(
page: page,
sort_by: String.to_existing_atom(sort_by),
sort_order: String.to_existing_atom(sort_order),
@@ -44,7 +44,7 @@ defmodule MicrowavepropWeb.QsoLive.Index do
@impl true
def handle_event("search", %{"search" => search}, socket) do
- {:noreply, push_patch(socket, to: ~p"/qsos?search=#{search}")}
+ {:noreply, push_patch(socket, to: ~p"/contacts?search=#{search}")}
end
@impl true
@@ -58,7 +58,7 @@ defmodule MicrowavepropWeb.QsoLive.Index do
params = %{sort_by: field, sort_order: new_order}
params = if socket.assigns.search == "", do: params, else: Map.put(params, :search, socket.assigns.search)
- {:noreply, push_patch(socket, to: ~p"/qsos?#{params}")}
+ {:noreply, push_patch(socket, to: ~p"/contacts?#{params}")}
end
defp validate_sort_field(field) when field in @sortable_fields, do: field
@@ -91,7 +91,7 @@ defmodule MicrowavepropWeb.QsoLive.Index do
class="input input-bordered input-sm w-64"
/>
- <.link :if={@search != ""} patch={~p"/qsos"} class="btn btn-sm btn-ghost">Clear
+ <.link :if={@search != ""} patch={~p"/contacts"} class="btn btn-sm btn-ghost">Clear
@@ -120,7 +120,7 @@ defmodule MicrowavepropWeb.QsoLive.Index do
"hover:bg-base-200 cursor-pointer",
reciprocals != [] && "bg-primary/5"
]}
- phx-click={JS.navigate(~p"/qsos/#{primary.id}")}
+ phx-click={JS.navigate(~p"/contacts/#{primary.id}")}
>
| ↳ |
{recip.station1} |
@@ -166,7 +166,7 @@ defmodule MicrowavepropWeb.QsoLive.Index do
<.link
:if={@page > 1}
patch={
- ~p"/qsos?page=#{@page - 1}&sort_by=#{@sort_by}&sort_order=#{@sort_order}&search=#{@search}"
+ ~p"/contacts?page=#{@page - 1}&sort_by=#{@sort_by}&sort_order=#{@sort_order}&search=#{@search}"
}
class="btn btn-sm btn-outline"
>
@@ -179,7 +179,7 @@ defmodule MicrowavepropWeb.QsoLive.Index do
<.link
:if={@page < @total_pages}
patch={
- ~p"/qsos?page=#{@page + 1}&sort_by=#{@sort_by}&sort_order=#{@sort_order}&search=#{@search}"
+ ~p"/contacts?page=#{@page + 1}&sort_by=#{@sort_by}&sort_order=#{@sort_order}&search=#{@search}"
}
class="btn btn-sm btn-outline"
>
diff --git a/lib/microwaveprop_web/live/contact_live/show.ex b/lib/microwaveprop_web/live/contact_live/show.ex
index 98f42fa1..965e72e7 100644
--- a/lib/microwaveprop_web/live/contact_live/show.ex
+++ b/lib/microwaveprop_web/live/contact_live/show.ex
@@ -1,4 +1,4 @@
-defmodule MicrowavepropWeb.QsoLive.Show do
+defmodule MicrowavepropWeb.ContactLive.Show do
@moduledoc false
use MicrowavepropWeb, :live_view
@@ -8,17 +8,17 @@ defmodule MicrowavepropWeb.QsoLive.Show do
@impl true
def mount(%{"id" => id}, _session, socket) do
- qso = Radio.get_qso!(id)
+ contact = Radio.get_contact!(id)
- weather = load_weather(qso)
- solar = load_solar(qso)
- hrrr = Weather.hrrr_for_qso(qso)
- terrain = Terrain.get_terrain_profile(qso.id)
+ weather = load_weather(contact)
+ solar = load_solar(contact)
+ hrrr = Weather.hrrr_for_contact(contact)
+ terrain = Terrain.get_terrain_profile(contact.id)
{:ok,
assign(socket,
- page_title: "#{qso.station1} / #{qso.station2}",
- qso: qso,
+ page_title: "#{contact.station1} / #{contact.station2}",
+ contact: contact,
surface_observations: weather.surface_observations,
soundings: weather.soundings,
solar: solar,
@@ -117,12 +117,12 @@ defmodule MicrowavepropWeb.QsoLive.Show do
defp sounding_sort_key("lifted_index"), do: &(&1.lifted_index || 0)
defp sounding_sort_key(_), do: fn s -> s.station.name || s.station.station_code end
- defp load_weather(qso) do
- lat = qso.pos1 && qso.pos1["lat"]
- lon = qso.pos1 && (qso.pos1["lon"] || qso.pos1["lng"])
+ defp load_weather(contact) do
+ lat = contact.pos1 && contact.pos1["lat"]
+ lon = contact.pos1 && (contact.pos1["lon"] || contact.pos1["lng"])
if lat && lon do
- Weather.weather_for_qso(%{lat: lat, lon: lon, timestamp: qso.qso_timestamp},
+ Weather.weather_for_contact(%{lat: lat, lon: lon, timestamp: contact.qso_timestamp},
radius_km: 300
)
else
@@ -130,8 +130,8 @@ defmodule MicrowavepropWeb.QsoLive.Show do
end
end
- defp load_solar(qso) do
- qso.qso_timestamp
+ defp load_solar(contact) do
+ contact.qso_timestamp
|> DateTime.to_date()
|> Weather.get_solar_index()
end
@@ -141,10 +141,10 @@ defmodule MicrowavepropWeb.QsoLive.Show do
~H"""
<.header>
- {@qso.station1} / {@qso.station2}
- <:subtitle>{Calendar.strftime(@qso.qso_timestamp, "%Y-%m-%d %H:%M UTC")}
+ {@contact.station1} / {@contact.station2}
+ <:subtitle>{Calendar.strftime(@contact.qso_timestamp, "%Y-%m-%d %H:%M UTC")}
<:actions>
- <.link navigate={~p"/qsos"} class="btn btn-sm btn-ghost">
+ <.link navigate={~p"/contacts"} class="btn btn-sm btn-ghost">
<.icon name="hero-arrow-left" class="w-4 h-4" /> Back to QSOs
@@ -154,19 +154,21 @@ defmodule MicrowavepropWeb.QsoLive.Show do
QSO Details
<.list>
- <:item title="Station 1">{@qso.station1}
- <:item title="Station 2">{@qso.station2}
- <:item title="Band">{@qso.band} MHz
- <:item title="Mode">{@qso.mode}
- <:item title="Grid 1">{@qso.grid1 || "—"}
- <:item title="Grid 2">{@qso.grid2 || "—"}
- <:item title="Distance">{@qso.distance_km || "—"} km
- <:item title="Timestamp">{Calendar.strftime(@qso.qso_timestamp, "%Y-%m-%d %H:%M UTC")}
- <:item :if={@qso.user_submitted} title="Source">
+ <:item title="Station 1">{@contact.station1}
+ <:item title="Station 2">{@contact.station2}
+ <:item title="Band">{@contact.band} MHz
+ <:item title="Mode">{@contact.mode}
+ <:item title="Grid 1">{@contact.grid1 || "—"}
+ <:item title="Grid 2">{@contact.grid2 || "—"}
+ <:item title="Distance">{@contact.distance_km || "—"} km
+ <:item title="Timestamp">
+ {Calendar.strftime(@contact.qso_timestamp, "%Y-%m-%d %H:%M UTC")}
+
+ <:item :if={@contact.user_submitted} title="Source">
User Submitted
- <:item :if={@qso.user_submitted && @qso.submitter_email} title="Submitter">
- {@qso.submitter_email}
+ <:item :if={@contact.user_submitted && @contact.submitter_email} title="Submitter">
+ {@contact.submitter_email}
diff --git a/lib/microwaveprop_web/live/submit_live.ex b/lib/microwaveprop_web/live/submit_live.ex
index 018ddca4..f5821178 100644
--- a/lib/microwaveprop_web/live/submit_live.ex
+++ b/lib/microwaveprop_web/live/submit_live.ex
@@ -3,8 +3,8 @@ defmodule MicrowavepropWeb.SubmitLive do
use MicrowavepropWeb, :live_view
alias Microwaveprop.Radio
- alias Microwaveprop.Radio.Qso
- alias Microwaveprop.Workers.QsoWeatherEnqueueWorker
+ alias Microwaveprop.Radio.Contact
+ alias Microwaveprop.Workers.ContactWeatherEnqueueWorker
@band_options [
{"1296 MHz", "1296"},
@@ -21,11 +21,11 @@ defmodule MicrowavepropWeb.SubmitLive do
@impl true
def mount(_params, _session, socket) do
- changeset = Radio.change_qso(%Qso{})
+ changeset = Radio.change_contact(%Contact{})
{:ok,
assign(socket,
- page_title: "Submit QSO",
+ page_title: "Submit Contact",
form: to_form(changeset),
band_options: @band_options,
mode_options: @mode_options,
@@ -34,10 +34,10 @@ defmodule MicrowavepropWeb.SubmitLive do
end
@impl true
- def handle_event("validate", %{"qso" => qso_params}, socket) do
+ def handle_event("validate", %{"contact" => contact_params}, socket) do
changeset =
- %Qso{}
- |> Radio.change_qso(qso_params)
+ %Contact{}
+ |> Radio.change_contact(contact_params)
|> Map.put(:action, :validate)
{:noreply, assign(socket, form: to_form(changeset))}
@@ -47,19 +47,19 @@ defmodule MicrowavepropWeb.SubmitLive do
# For broader IP-based rate limiting, use a reverse proxy (nginx limit_req, Cloudflare).
@submission_cooldown_ms 30_000
- def handle_event("save", %{"qso" => qso_params}, socket) do
+ def handle_event("save", %{"contact" => contact_params}, socket) do
if recently_submitted?(socket) do
- {:noreply, put_flash(socket, :error, "Please wait before submitting another QSO.")}
+ {:noreply, put_flash(socket, :error, "Please wait before submitting another contact.")}
else
- case Radio.create_qso(qso_params) do
- {:ok, qso} ->
- QsoWeatherEnqueueWorker.enqueue_for_qso(qso)
+ case Radio.create_contact(contact_params) do
+ {:ok, contact} ->
+ ContactWeatherEnqueueWorker.enqueue_for_contact(contact)
{:noreply,
socket
|> assign(submitted_at: System.monotonic_time(:millisecond))
- |> put_flash(:info, "QSO submitted successfully!")
- |> push_navigate(to: ~p"/qsos/#{qso.id}")}
+ |> put_flash(:info, "Contact submitted successfully!")
+ |> push_navigate(to: ~p"/contacts/#{contact.id}")}
{:error, changeset} ->
{:noreply, assign(socket, form: to_form(changeset))}
@@ -79,11 +79,11 @@ defmodule MicrowavepropWeb.SubmitLive do
~H"""
<.header>
- Submit QSO
- <:subtitle>Submit a microwave QSO for propagation analysis
+ Submit Contact
+ <:subtitle>Submit a microwave contact for propagation analysis
- <.form for={@form} id="qso-form" phx-change="validate" phx-submit="save" class="space-y-4">
+ <.form for={@form} id="contact-form" phx-change="validate" phx-submit="save" class="space-y-4">
<.input field={@form[:station1]} type="text" label="Station 1" placeholder="W5XD" />
<.input
@@ -133,7 +133,7 @@ defmodule MicrowavepropWeb.SubmitLive do
/>
- <.button phx-disable-with="Submitting..." class="btn-primary">Submit QSO
+ <.button phx-disable-with="Submitting..." class="btn-primary">Submit Contact
diff --git a/lib/microwaveprop_web/router.ex b/lib/microwaveprop_web/router.ex
index 0ff1e2a8..73935255 100644
--- a/lib/microwaveprop_web/router.ex
+++ b/lib/microwaveprop_web/router.ex
@@ -21,9 +21,13 @@ defmodule MicrowavepropWeb.Router do
live "/submit", SubmitLive
live "/map", MapLive
- live "/qsos", QsoLive.Index
- live "/qsos/:id", QsoLive.Show
+ live "/contacts", ContactLive.Index
+ live "/contacts/:id", ContactLive.Show
live "/algo", AlgoLive
+
+ # Redirect old /qsos routes
+ get "/qsos", PageController, :redirect_contacts
+ get "/qsos/:id", PageController, :redirect_contact
end
# Other scopes may use custom stacks.
diff --git a/lib/mix/tasks/propagation_train.ex b/lib/mix/tasks/propagation_train.ex
index 1e9eba4c..d3731ae1 100644
--- a/lib/mix/tasks/propagation_train.ex
+++ b/lib/mix/tasks/propagation_train.ex
@@ -118,7 +118,7 @@ defmodule Mix.Tasks.PropagationTrain do
IO.puts("=" <> String.duplicate("─", 50))
- {ft_features, ft_targets, ft_counts} = load_qso_data()
+ {ft_features, ft_targets, ft_counts} = load_contact_data()
ft_n = elem(Nx.shape(ft_features), 0)
IO.puts("Loaded #{ft_n} QSO training samples:")
print_band_counts(ft_counts)
@@ -247,7 +247,7 @@ defmodule Mix.Tasks.PropagationTrain do
# ── Phase 2 data: QSO-HRRR + solar + sounding ────────────────────
- defp load_qso_data do
+ defp load_contact_data do
IO.puts(" Running QSO-HRRR-solar-sounding join query...")
sql = """
@@ -326,7 +326,7 @@ defmodule Mix.Tasks.PropagationTrain do
{feature_rows, target_rows} =
percentile_rows
- |> Enum.map(fn {row, percentile} -> encode_qso_row(row, percentile) end)
+ |> Enum.map(fn {row, percentile} -> encode_contact_row(row, percentile) end)
|> Enum.unzip()
IO.puts(" After filtering: #{length(feature_rows)} training samples")
@@ -334,7 +334,7 @@ defmodule Mix.Tasks.PropagationTrain do
{Nx.tensor(feature_rows, type: :f32), Nx.tensor(target_rows, type: :f32), band_counts}
end
- defp encode_qso_row(
+ defp encode_contact_row(
[
band_mhz,
_dist,
diff --git a/lib/mix/tasks/reset_enrichment.ex b/lib/mix/tasks/reset_enrichment.ex
index d7b56a33..fcacdc76 100644
--- a/lib/mix/tasks/reset_enrichment.ex
+++ b/lib/mix/tasks/reset_enrichment.ex
@@ -15,7 +15,7 @@ defmodule Mix.Tasks.ResetEnrichment do
import Ecto.Query
- alias Microwaveprop.Radio.Qso
+ alias Microwaveprop.Radio.Contact
alias Microwaveprop.Repo
@impl Mix.Task
@@ -23,7 +23,7 @@ defmodule Mix.Tasks.ResetEnrichment do
Mix.Task.run("app.start")
{count, _} =
- Qso
+ Contact
|> where([q], q.weather_queued == true or q.hrrr_queued == true or q.iemre_queued == true)
|> Repo.update_all(set: [weather_queued: false, hrrr_queued: false, iemre_queued: false])
diff --git a/test/microwaveprop/radio/contact_submission_test.exs b/test/microwaveprop/radio/contact_submission_test.exs
index f1685036..f5fbe499 100644
--- a/test/microwaveprop/radio/contact_submission_test.exs
+++ b/test/microwaveprop/radio/contact_submission_test.exs
@@ -1,7 +1,7 @@
-defmodule Microwaveprop.Radio.QsoSubmissionTest do
+defmodule Microwaveprop.Radio.ContactSubmissionTest do
use Microwaveprop.DataCase, async: true
- alias Microwaveprop.Radio.Qso
+ alias Microwaveprop.Radio.Contact
@valid_attrs %{
station1: "W5XD",
@@ -16,12 +16,12 @@ defmodule Microwaveprop.Radio.QsoSubmissionTest do
describe "submission_changeset/2" do
test "valid attrs produce a valid changeset" do
- changeset = Qso.submission_changeset(%Qso{}, @valid_attrs)
+ changeset = Contact.submission_changeset(%Contact{}, @valid_attrs)
assert changeset.valid?
end
test "requires station1, station2, qso_timestamp, mode, band, grid1, grid2, submitter_email" do
- changeset = Qso.submission_changeset(%Qso{}, %{})
+ changeset = Contact.submission_changeset(%Contact{}, %{})
errors = errors_on(changeset)
assert errors[:station1]
@@ -36,59 +36,59 @@ defmodule Microwaveprop.Radio.QsoSubmissionTest do
test "rejects invalid grid1" do
attrs = Map.put(@valid_attrs, :grid1, "ZZ99")
- changeset = Qso.submission_changeset(%Qso{}, attrs)
+ changeset = Contact.submission_changeset(%Contact{}, attrs)
assert "is not a valid Maidenhead grid square" in errors_on(changeset).grid1
end
test "rejects invalid grid2" do
attrs = Map.put(@valid_attrs, :grid2, "ZZ99")
- changeset = Qso.submission_changeset(%Qso{}, attrs)
+ changeset = Contact.submission_changeset(%Contact{}, attrs)
assert "is not a valid Maidenhead grid square" in errors_on(changeset).grid2
end
test "accepts valid 6-char grids" do
attrs = %{@valid_attrs | grid1: "EM12ab", grid2: "EM00cd"}
- changeset = Qso.submission_changeset(%Qso{}, attrs)
+ changeset = Contact.submission_changeset(%Contact{}, attrs)
assert changeset.valid?
end
test "rejects invalid mode" do
attrs = Map.put(@valid_attrs, :mode, "RTTY")
- changeset = Qso.submission_changeset(%Qso{}, attrs)
+ changeset = Contact.submission_changeset(%Contact{}, attrs)
assert errors_on(changeset).mode
end
test "accepts valid modes" do
for mode <- ~w(CW SSB FM FT8 FT4) do
attrs = Map.put(@valid_attrs, :mode, mode)
- changeset = Qso.submission_changeset(%Qso{}, attrs)
+ changeset = Contact.submission_changeset(%Contact{}, attrs)
assert changeset.valid?, "Expected mode #{mode} to be valid"
end
end
test "rejects invalid band" do
attrs = Map.put(@valid_attrs, :band, "144")
- changeset = Qso.submission_changeset(%Qso{}, attrs)
+ changeset = Contact.submission_changeset(%Contact{}, attrs)
assert errors_on(changeset).band
end
test "accepts valid microwave bands" do
for band <- ~w(1296 2304 3456 5760 10000 24000 47000 75000) do
attrs = Map.put(@valid_attrs, :band, band)
- changeset = Qso.submission_changeset(%Qso{}, attrs)
+ changeset = Contact.submission_changeset(%Contact{}, attrs)
assert changeset.valid?, "Expected band #{band} to be valid"
end
end
test "rejects email without @" do
attrs = Map.put(@valid_attrs, :submitter_email, "notanemail")
- changeset = Qso.submission_changeset(%Qso{}, attrs)
+ changeset = Contact.submission_changeset(%Contact{}, attrs)
assert errors_on(changeset).submitter_email
end
test "does not cast user_submitted" do
attrs = Map.put(@valid_attrs, :user_submitted, true)
- changeset = Qso.submission_changeset(%Qso{}, attrs)
+ changeset = Contact.submission_changeset(%Contact{}, attrs)
refute Ecto.Changeset.get_change(changeset, :user_submitted)
end
end
diff --git a/test/microwaveprop/radio/contact_test.exs b/test/microwaveprop/radio/contact_test.exs
index 94ba091e..d1e8dd4f 100644
--- a/test/microwaveprop/radio/contact_test.exs
+++ b/test/microwaveprop/radio/contact_test.exs
@@ -1,7 +1,7 @@
-defmodule Microwaveprop.Radio.QsoTest do
+defmodule Microwaveprop.Radio.ContactTest do
use Microwaveprop.DataCase, async: true
- alias Microwaveprop.Radio.Qso
+ alias Microwaveprop.Radio.Contact
@valid_attrs %{
station1: "W1AW",
@@ -18,12 +18,12 @@ defmodule Microwaveprop.Radio.QsoTest do
describe "changeset/2" do
test "valid attributes" do
- changeset = Qso.changeset(%Qso{}, @valid_attrs)
+ changeset = Contact.changeset(%Contact{}, @valid_attrs)
assert changeset.valid?
end
test "requires station1, station2, qso_timestamp, mode, and band" do
- changeset = Qso.changeset(%Qso{}, %{})
+ changeset = Contact.changeset(%Contact{}, %{})
assert %{
station1: ["can't be blank"],
@@ -35,13 +35,13 @@ defmodule Microwaveprop.Radio.QsoTest do
end
test "persists to the database" do
- changeset = Qso.changeset(%Qso{}, @valid_attrs)
- assert {:ok, qso} = Repo.insert(changeset)
- assert qso.station1 == "W1AW"
- assert qso.station2 == "K3LR"
- assert qso.mode == "SSB"
- assert Decimal.equal?(qso.band, Decimal.new("144.2"))
- assert Decimal.equal?(qso.distance_km, Decimal.new("623.4"))
+ changeset = Contact.changeset(%Contact{}, @valid_attrs)
+ assert {:ok, contact} = Repo.insert(changeset)
+ assert contact.station1 == "W1AW"
+ assert contact.station2 == "K3LR"
+ assert contact.mode == "SSB"
+ assert Decimal.equal?(contact.band, Decimal.new("144.2"))
+ assert Decimal.equal?(contact.distance_km, Decimal.new("623.4"))
end
end
end
diff --git a/test/microwaveprop/radio_test.exs b/test/microwaveprop/radio_test.exs
index 1f84ea87..ec47ae9c 100644
--- a/test/microwaveprop/radio_test.exs
+++ b/test/microwaveprop/radio_test.exs
@@ -2,9 +2,9 @@ defmodule Microwaveprop.RadioTest do
use Microwaveprop.DataCase, async: true
alias Microwaveprop.Radio
- alias Microwaveprop.Radio.Qso
+ alias Microwaveprop.Radio.Contact
- defp create_qso(attrs \\ %{}) do
+ defp create_contact(attrs \\ %{}) do
default = %{
station1: "W5XD",
station2: "K5TR",
@@ -18,17 +18,17 @@ defmodule Microwaveprop.RadioTest do
distance_km: Decimal.new("295")
}
- {:ok, qso} =
- %Qso{}
- |> Qso.changeset(Map.merge(default, attrs))
+ {:ok, contact} =
+ %Contact{}
+ |> Contact.changeset(Map.merge(default, attrs))
|> Repo.insert()
- qso
+ contact
end
- describe "list_qsos/1" do
+ describe "list_contacts/1" do
test "returns empty page when no QSOs exist" do
- result = Radio.list_qsos()
+ result = Radio.list_contacts()
assert result.entries == []
assert result.page == 1
@@ -39,10 +39,10 @@ defmodule Microwaveprop.RadioTest do
test "paginates 20 per page, ordered by qso_timestamp desc" do
for i <- 1..25 do
ts = DateTime.add(~U[2026-01-01 00:00:00Z], i * 3600, :second)
- create_qso(%{qso_timestamp: ts})
+ create_contact(%{qso_timestamp: ts})
end
- result = Radio.list_qsos()
+ result = Radio.list_contacts()
assert length(result.entries) == 20
assert result.page == 1
@@ -57,10 +57,10 @@ defmodule Microwaveprop.RadioTest do
test "returns second page" do
for i <- 1..25 do
ts = DateTime.add(~U[2026-01-01 00:00:00Z], i * 3600, :second)
- create_qso(%{qso_timestamp: ts})
+ create_contact(%{qso_timestamp: ts})
end
- result = Radio.list_qsos(page: 2)
+ result = Radio.list_contacts(page: 2)
assert length(result.entries) == 5
assert result.page == 2
@@ -68,60 +68,60 @@ defmodule Microwaveprop.RadioTest do
end
end
- describe "list_qsos/1 sorting" do
+ describe "list_contacts/1 sorting" do
test "sorts by station1 ascending" do
- create_qso(%{station1: "ZZ9ZZ"})
- create_qso(%{station1: "AA1AA"})
+ create_contact(%{station1: "ZZ9ZZ"})
+ create_contact(%{station1: "AA1AA"})
- result = Radio.list_qsos(sort_by: :station1, sort_order: :asc)
+ result = Radio.list_contacts(sort_by: :station1, sort_order: :asc)
stations = Enum.map(result.entries, & &1.station1)
assert stations == ["AA1AA", "ZZ9ZZ"]
end
test "sorts by distance_km descending" do
- create_qso(%{distance_km: Decimal.new("100"), station1: "A1A"})
- create_qso(%{distance_km: Decimal.new("500"), station1: "B2B"})
+ create_contact(%{distance_km: Decimal.new("100"), station1: "A1A"})
+ create_contact(%{distance_km: Decimal.new("500"), station1: "B2B"})
- result = Radio.list_qsos(sort_by: :distance_km, sort_order: :desc)
+ result = Radio.list_contacts(sort_by: :distance_km, sort_order: :desc)
distances = Enum.map(result.entries, & &1.distance_km)
assert distances == [Decimal.new("500"), Decimal.new("100")]
end
test "invalid sort_by falls back to qso_timestamp desc" do
- early = create_qso(%{qso_timestamp: ~U[2026-01-01 00:00:00Z]})
- late = create_qso(%{qso_timestamp: ~U[2026-03-01 00:00:00Z]})
+ early = create_contact(%{qso_timestamp: ~U[2026-01-01 00:00:00Z]})
+ late = create_contact(%{qso_timestamp: ~U[2026-03-01 00:00:00Z]})
- result = Radio.list_qsos(sort_by: :bogus)
+ result = Radio.list_contacts(sort_by: :bogus)
ids = Enum.map(result.entries, & &1.id)
assert ids == [late.id, early.id]
end
end
- describe "unprocessed_qsos/1" do
+ describe "unprocessed_contacts/1" do
test "returns QSOs where weather_queued is false and pos1 is not nil" do
- q1 = create_qso(%{station1: "W5XD", qso_timestamp: ~U[2026-03-28 12:00:00Z]})
- _q2 = create_qso(%{station1: "K5TR", pos1: nil, qso_timestamp: ~U[2026-03-28 13:00:00Z]})
+ q1 = create_contact(%{station1: "W5XD", qso_timestamp: ~U[2026-03-28 12:00:00Z]})
+ _q2 = create_contact(%{station1: "K5TR", pos1: nil, qso_timestamp: ~U[2026-03-28 13:00:00Z]})
- results = Radio.unprocessed_qsos()
+ results = Radio.unprocessed_contacts()
ids = Enum.map(results, & &1.id)
assert q1.id in ids
end
test "excludes QSOs already marked as weather_queued" do
- q = create_qso()
+ q = create_contact()
Radio.mark_weather_queued!([q.id])
- assert Radio.unprocessed_qsos() == []
+ assert Radio.unprocessed_contacts() == []
end
test "orders by qso_timestamp ascending" do
- q_late = create_qso(%{station1: "LATE", qso_timestamp: ~U[2026-03-28 20:00:00Z]})
- q_early = create_qso(%{station1: "EARLY", qso_timestamp: ~U[2026-03-28 10:00:00Z]})
+ q_late = create_contact(%{station1: "LATE", qso_timestamp: ~U[2026-03-28 20:00:00Z]})
+ q_early = create_contact(%{station1: "EARLY", qso_timestamp: ~U[2026-03-28 10:00:00Z]})
- results = Radio.unprocessed_qsos()
+ results = Radio.unprocessed_contacts()
ids = Enum.map(results, & &1.id)
assert ids == [q_early.id, q_late.id]
end
@@ -129,32 +129,32 @@ defmodule Microwaveprop.RadioTest do
test "respects limit parameter" do
for i <- 1..5 do
ts = DateTime.add(~U[2026-01-01 00:00:00Z], i * 3600, :second)
- create_qso(%{qso_timestamp: ts})
+ create_contact(%{qso_timestamp: ts})
end
- assert length(Radio.unprocessed_qsos(3)) == 3
+ assert length(Radio.unprocessed_contacts(3)) == 3
end
end
describe "mark_weather_queued!/1" do
test "sets weather_queued to true for given IDs" do
- q1 = create_qso(%{station1: "A1A"})
- q2 = create_qso(%{station1: "B2B"})
+ q1 = create_contact(%{station1: "A1A"})
+ q2 = create_contact(%{station1: "B2B"})
Radio.mark_weather_queued!([q1.id, q2.id])
- assert Repo.get!(Qso, q1.id).weather_queued == true
- assert Repo.get!(Qso, q2.id).weather_queued == true
+ assert Repo.get!(Contact, q1.id).weather_queued == true
+ assert Repo.get!(Contact, q2.id).weather_queued == true
end
test "does not affect other QSOs" do
- q1 = create_qso(%{station1: "A1A"})
- q2 = create_qso(%{station1: "B2B"})
+ q1 = create_contact(%{station1: "A1A"})
+ q2 = create_contact(%{station1: "B2B"})
Radio.mark_weather_queued!([q1.id])
- assert Repo.get!(Qso, q1.id).weather_queued == true
- assert Repo.get!(Qso, q2.id).weather_queued == false
+ assert Repo.get!(Contact, q1.id).weather_queued == true
+ assert Repo.get!(Contact, q2.id).weather_queued == false
end
test "handles empty list" do
@@ -176,72 +176,72 @@ defmodule Microwaveprop.RadioTest do
describe "backfill_distances/1" do
test "calculates and stores distance for QSOs missing distance_km" do
- qso = create_qso(%{distance_km: nil})
- assert is_nil(qso.distance_km)
+ contact = create_contact(%{distance_km: nil})
+ assert is_nil(contact.distance_km)
- Radio.backfill_distances([qso])
+ Radio.backfill_distances([contact])
- updated = Repo.get!(Qso, qso.id)
+ updated = Repo.get!(Contact, contact.id)
assert_in_delta Decimal.to_float(updated.distance_km), 295, 5
end
test "skips QSOs that already have a distance" do
- qso = create_qso(%{distance_km: Decimal.new("100")})
+ contact = create_contact(%{distance_km: Decimal.new("100")})
- Radio.backfill_distances([qso])
+ Radio.backfill_distances([contact])
- updated = Repo.get!(Qso, qso.id)
+ updated = Repo.get!(Contact, contact.id)
assert updated.distance_km == Decimal.new("100")
end
test "skips QSOs missing pos2" do
- qso = create_qso(%{distance_km: nil, pos2: nil})
+ contact = create_contact(%{distance_km: nil, pos2: nil})
- Radio.backfill_distances([qso])
+ Radio.backfill_distances([contact])
- updated = Repo.get!(Qso, qso.id)
+ updated = Repo.get!(Contact, contact.id)
assert is_nil(updated.distance_km)
end
end
- describe "get_qso!/1" do
+ describe "get_contact!/1" do
test "returns a QSO by ID" do
- qso = create_qso()
+ contact = create_contact()
- found = Radio.get_qso!(qso.id)
- assert found.id == qso.id
+ found = Radio.get_contact!(contact.id)
+ assert found.id == contact.id
assert found.station1 == "W5XD"
end
test "raises Ecto.NoResultsError on bad ID" do
assert_raise Ecto.NoResultsError, fn ->
- Radio.get_qso!(Ecto.UUID.generate())
+ Radio.get_contact!(Ecto.UUID.generate())
end
end
end
- describe "unprocessed_hrrr_qsos/1" do
+ describe "unprocessed_hrrr_contacts/1" do
test "returns QSOs where hrrr_queued is false and pos1 is not nil" do
- q1 = create_qso(%{station1: "W5XD", qso_timestamp: ~U[2026-03-28 12:00:00Z]})
- _q2 = create_qso(%{station1: "K5TR", pos1: nil, qso_timestamp: ~U[2026-03-28 13:00:00Z]})
+ q1 = create_contact(%{station1: "W5XD", qso_timestamp: ~U[2026-03-28 12:00:00Z]})
+ _q2 = create_contact(%{station1: "K5TR", pos1: nil, qso_timestamp: ~U[2026-03-28 13:00:00Z]})
- results = Radio.unprocessed_hrrr_qsos()
+ results = Radio.unprocessed_hrrr_contacts()
ids = Enum.map(results, & &1.id)
assert q1.id in ids
end
test "excludes QSOs already marked as hrrr_queued" do
- q = create_qso()
+ q = create_contact()
Radio.mark_hrrr_queued!([q.id])
- assert Radio.unprocessed_hrrr_qsos() == []
+ assert Radio.unprocessed_hrrr_contacts() == []
end
test "orders by qso_timestamp ascending" do
- q_late = create_qso(%{station1: "LATE", qso_timestamp: ~U[2026-03-28 20:00:00Z]})
- q_early = create_qso(%{station1: "EARLY", qso_timestamp: ~U[2026-03-28 10:00:00Z]})
+ q_late = create_contact(%{station1: "LATE", qso_timestamp: ~U[2026-03-28 20:00:00Z]})
+ q_early = create_contact(%{station1: "EARLY", qso_timestamp: ~U[2026-03-28 10:00:00Z]})
- results = Radio.unprocessed_hrrr_qsos()
+ results = Radio.unprocessed_hrrr_contacts()
ids = Enum.map(results, & &1.id)
assert ids == [q_early.id, q_late.id]
end
@@ -249,32 +249,32 @@ defmodule Microwaveprop.RadioTest do
test "respects limit parameter" do
for i <- 1..5 do
ts = DateTime.add(~U[2026-01-01 00:00:00Z], i * 3600, :second)
- create_qso(%{qso_timestamp: ts})
+ create_contact(%{qso_timestamp: ts})
end
- assert length(Radio.unprocessed_hrrr_qsos(3)) == 3
+ assert length(Radio.unprocessed_hrrr_contacts(3)) == 3
end
end
describe "mark_hrrr_queued!/1" do
test "sets hrrr_queued to true for given IDs" do
- q1 = create_qso(%{station1: "A1A"})
- q2 = create_qso(%{station1: "B2B"})
+ q1 = create_contact(%{station1: "A1A"})
+ q2 = create_contact(%{station1: "B2B"})
Radio.mark_hrrr_queued!([q1.id, q2.id])
- assert Repo.get!(Qso, q1.id).hrrr_queued == true
- assert Repo.get!(Qso, q2.id).hrrr_queued == true
+ assert Repo.get!(Contact, q1.id).hrrr_queued == true
+ assert Repo.get!(Contact, q2.id).hrrr_queued == true
end
test "does not affect other QSOs" do
- q1 = create_qso(%{station1: "A1A"})
- q2 = create_qso(%{station1: "B2B"})
+ q1 = create_contact(%{station1: "A1A"})
+ q2 = create_contact(%{station1: "B2B"})
Radio.mark_hrrr_queued!([q1.id])
- assert Repo.get!(Qso, q1.id).hrrr_queued == true
- assert Repo.get!(Qso, q2.id).hrrr_queued == false
+ assert Repo.get!(Contact, q1.id).hrrr_queued == true
+ assert Repo.get!(Contact, q2.id).hrrr_queued == false
end
test "handles empty list" do
@@ -282,30 +282,30 @@ defmodule Microwaveprop.RadioTest do
end
end
- describe "unprocessed_terrain_qsos/1" do
+ describe "unprocessed_terrain_contacts/1" do
test "returns QSOs where terrain_queued is false and both pos1 and pos2 are not nil" do
- q1 = create_qso(%{station1: "W5XD", qso_timestamp: ~U[2026-03-28 12:00:00Z]})
- _q2 = create_qso(%{station1: "NOPOS1", pos1: nil, qso_timestamp: ~U[2026-03-28 13:00:00Z]})
- _q3 = create_qso(%{station1: "NOPOS2", pos2: nil, qso_timestamp: ~U[2026-03-28 14:00:00Z]})
+ q1 = create_contact(%{station1: "W5XD", qso_timestamp: ~U[2026-03-28 12:00:00Z]})
+ _q2 = create_contact(%{station1: "NOPOS1", pos1: nil, qso_timestamp: ~U[2026-03-28 13:00:00Z]})
+ _q3 = create_contact(%{station1: "NOPOS2", pos2: nil, qso_timestamp: ~U[2026-03-28 14:00:00Z]})
- results = Radio.unprocessed_terrain_qsos()
+ results = Radio.unprocessed_terrain_contacts()
ids = Enum.map(results, & &1.id)
assert q1.id in ids
assert length(ids) == 1
end
test "excludes QSOs already marked as terrain_queued" do
- q = create_qso()
+ q = create_contact()
Radio.mark_terrain_queued!([q.id])
- assert Radio.unprocessed_terrain_qsos() == []
+ assert Radio.unprocessed_terrain_contacts() == []
end
test "orders by qso_timestamp ascending" do
- q_late = create_qso(%{station1: "LATE", qso_timestamp: ~U[2026-03-28 20:00:00Z]})
- q_early = create_qso(%{station1: "EARLY", qso_timestamp: ~U[2026-03-28 10:00:00Z]})
+ q_late = create_contact(%{station1: "LATE", qso_timestamp: ~U[2026-03-28 20:00:00Z]})
+ q_early = create_contact(%{station1: "EARLY", qso_timestamp: ~U[2026-03-28 10:00:00Z]})
- results = Radio.unprocessed_terrain_qsos()
+ results = Radio.unprocessed_terrain_contacts()
ids = Enum.map(results, & &1.id)
assert ids == [q_early.id, q_late.id]
end
@@ -313,32 +313,32 @@ defmodule Microwaveprop.RadioTest do
test "respects limit parameter" do
for i <- 1..5 do
ts = DateTime.add(~U[2026-01-01 00:00:00Z], i * 3600, :second)
- create_qso(%{qso_timestamp: ts})
+ create_contact(%{qso_timestamp: ts})
end
- assert length(Radio.unprocessed_terrain_qsos(3)) == 3
+ assert length(Radio.unprocessed_terrain_contacts(3)) == 3
end
end
describe "mark_terrain_queued!/1" do
test "sets terrain_queued to true for given IDs" do
- q1 = create_qso(%{station1: "A1A"})
- q2 = create_qso(%{station1: "B2B"})
+ q1 = create_contact(%{station1: "A1A"})
+ q2 = create_contact(%{station1: "B2B"})
Radio.mark_terrain_queued!([q1.id, q2.id])
- assert Repo.get!(Qso, q1.id).terrain_queued == true
- assert Repo.get!(Qso, q2.id).terrain_queued == true
+ assert Repo.get!(Contact, q1.id).terrain_queued == true
+ assert Repo.get!(Contact, q2.id).terrain_queued == true
end
test "does not affect other QSOs" do
- q1 = create_qso(%{station1: "A1A"})
- q2 = create_qso(%{station1: "B2B"})
+ q1 = create_contact(%{station1: "A1A"})
+ q2 = create_contact(%{station1: "B2B"})
Radio.mark_terrain_queued!([q1.id])
- assert Repo.get!(Qso, q1.id).terrain_queued == true
- assert Repo.get!(Qso, q2.id).terrain_queued == false
+ assert Repo.get!(Contact, q1.id).terrain_queued == true
+ assert Repo.get!(Contact, q2.id).terrain_queued == false
end
test "handles empty list" do
@@ -346,15 +346,15 @@ defmodule Microwaveprop.RadioTest do
end
end
- describe "qso_path_points/1" do
+ describe "contact_path_points/1" do
test "returns pos1, midpoint, and pos2 when both positions exist" do
- qso =
- create_qso(%{
+ contact =
+ create_contact(%{
pos1: %{"lat" => 32.9, "lon" => -97.0},
pos2: %{"lat" => 30.3, "lon" => -97.7}
})
- points = Radio.qso_path_points(qso)
+ points = Radio.contact_path_points(contact)
assert length(points) == 3
[{lat1, lon1}, {mid_lat, mid_lon}, {lat2, lon2}] = points
@@ -367,44 +367,44 @@ defmodule Microwaveprop.RadioTest do
end
test "returns only pos1 when pos2 is nil" do
- qso = create_qso(%{pos2: nil})
+ contact = create_contact(%{pos2: nil})
- points = Radio.qso_path_points(qso)
+ points = Radio.contact_path_points(contact)
assert points == [{32.9, -97.0}]
end
test "returns empty list when pos1 is nil" do
- qso = create_qso(%{pos1: nil})
+ contact = create_contact(%{pos1: nil})
- assert Radio.qso_path_points(qso) == []
+ assert Radio.contact_path_points(contact) == []
end
test "handles lng key in position maps" do
- qso =
- create_qso(%{
+ contact =
+ create_contact(%{
pos1: %{"lat" => 32.9, "lng" => -97.0},
pos2: %{"lat" => 30.3, "lng" => -97.7}
})
- points = Radio.qso_path_points(qso)
+ points = Radio.contact_path_points(contact)
assert length(points) == 3
end
end
- describe "change_qso/2" do
+ describe "change_contact/2" do
test "returns a submission changeset" do
- changeset = Radio.change_qso(%Qso{})
+ changeset = Radio.change_contact(%Contact{})
assert %Ecto.Changeset{} = changeset
end
test "accepts attrs" do
- changeset = Radio.change_qso(%Qso{}, %{station1: "W5XD"})
+ changeset = Radio.change_contact(%Contact{}, %{station1: "W5XD"})
assert Ecto.Changeset.get_change(changeset, :station1) == "W5XD"
end
end
- describe "create_qso/1" do
+ describe "create_contact/1" do
@valid_submission %{
station1: "W5XD",
station2: "K5TR",
@@ -417,38 +417,38 @@ defmodule Microwaveprop.RadioTest do
}
test "creates QSO with computed positions and distance" do
- assert {:ok, qso} = Radio.create_qso(@valid_submission)
+ assert {:ok, contact} = Radio.create_contact(@valid_submission)
- assert qso.station1 == "W5XD"
- assert qso.user_submitted == true
- assert qso.pos1["lat"]
- assert qso.pos1["lon"]
- assert qso.pos2["lat"]
- assert qso.pos2["lon"]
- assert qso.distance_km
+ assert contact.station1 == "W5XD"
+ assert contact.user_submitted == true
+ assert contact.pos1["lat"]
+ assert contact.pos1["lon"]
+ assert contact.pos2["lat"]
+ assert contact.pos2["lon"]
+ assert contact.distance_km
end
test "computes correct position from grid" do
- assert {:ok, qso} = Radio.create_qso(@valid_submission)
+ assert {:ok, contact} = Radio.create_contact(@valid_submission)
# EM12 center: lat = 4*10 - 90 + 1*1 + 0.5 = -48.5... wait
# E=4, M=12 -> lon = 4*20 - 180 + 1*2 + 1 = -97, lat = 12*10 - 90 + 2*1 + 0.5 = 32.5
- assert_in_delta qso.pos1["lat"], 32.5, 0.1
- assert_in_delta qso.pos1["lon"], -97.0, 0.1
+ assert_in_delta contact.pos1["lat"], 32.5, 0.1
+ assert_in_delta contact.pos1["lon"], -97.0, 0.1
end
test "sets user_submitted to true" do
- assert {:ok, qso} = Radio.create_qso(@valid_submission)
- assert qso.user_submitted == true
+ assert {:ok, contact} = Radio.create_contact(@valid_submission)
+ assert contact.user_submitted == true
end
test "returns error changeset for invalid data" do
- assert {:error, %Ecto.Changeset{}} = Radio.create_qso(%{})
+ assert {:error, %Ecto.Changeset{}} = Radio.create_contact(%{})
end
test "returns error changeset for invalid grid" do
attrs = Map.put(@valid_submission, :grid1, "ZZ99")
- assert {:error, changeset} = Radio.create_qso(attrs)
+ assert {:error, changeset} = Radio.create_contact(attrs)
assert errors_on(changeset).grid1
end
end
diff --git a/test/microwaveprop/terrain/terrain_profile_test.exs b/test/microwaveprop/terrain/terrain_profile_test.exs
index e8d4c90f..7e5f227b 100644
--- a/test/microwaveprop/terrain/terrain_profile_test.exs
+++ b/test/microwaveprop/terrain/terrain_profile_test.exs
@@ -1,7 +1,7 @@
defmodule Microwaveprop.Terrain.TerrainProfileTest do
use Microwaveprop.DataCase, async: true
- alias Microwaveprop.Radio.Qso
+ alias Microwaveprop.Radio.Contact
alias Microwaveprop.Terrain.TerrainProfile
@qso_attrs %{
@@ -14,21 +14,21 @@ defmodule Microwaveprop.Terrain.TerrainProfileTest do
pos2: %{"lat" => 30.3, "lon" => -97.7}
}
- defp create_qso do
- {:ok, qso} =
- %Qso{}
- |> Qso.changeset(@qso_attrs)
+ defp create_contact do
+ {:ok, contact} =
+ %Contact{}
+ |> Contact.changeset(@qso_attrs)
|> Repo.insert()
- qso
+ contact
end
describe "changeset/2" do
test "valid with required fields" do
- qso = create_qso()
+ contact = create_contact()
attrs = %{
- qso_id: qso.id,
+ qso_id: contact.id,
sample_count: 65,
path_points: [%{"lat" => 32.9, "lon" => -97.0, "elev" => 200.0, "dist_km" => 0.0}],
verdict: "CLEAR"
@@ -50,10 +50,10 @@ defmodule Microwaveprop.Terrain.TerrainProfileTest do
end
test "valid with all optional fields" do
- qso = create_qso()
+ contact = create_contact()
attrs = %{
- qso_id: qso.id,
+ qso_id: contact.id,
sample_count: 65,
path_points: [%{"lat" => 32.9, "lon" => -97.0, "elev" => 200.0, "dist_km" => 0.0}],
verdict: "BLOCKED",
@@ -69,10 +69,10 @@ defmodule Microwaveprop.Terrain.TerrainProfileTest do
end
test "enforces unique qso_id constraint on insert" do
- qso = create_qso()
+ contact = create_contact()
attrs = %{
- qso_id: qso.id,
+ qso_id: contact.id,
sample_count: 65,
path_points: [],
verdict: "CLEAR"
diff --git a/test/microwaveprop/terrain_test.exs b/test/microwaveprop/terrain_test.exs
index 9da90bc4..342e64c3 100644
--- a/test/microwaveprop/terrain_test.exs
+++ b/test/microwaveprop/terrain_test.exs
@@ -1,7 +1,7 @@
defmodule Microwaveprop.TerrainTest do
use Microwaveprop.DataCase, async: true
- alias Microwaveprop.Radio.Qso
+ alias Microwaveprop.Radio.Contact
alias Microwaveprop.Terrain
@qso_attrs %{
@@ -14,18 +14,18 @@ defmodule Microwaveprop.TerrainTest do
pos2: %{"lat" => 30.3, "lon" => -97.7}
}
- defp create_qso do
- {:ok, qso} =
- %Qso{}
- |> Qso.changeset(@qso_attrs)
+ defp create_contact do
+ {:ok, contact} =
+ %Contact{}
+ |> Contact.changeset(@qso_attrs)
|> Repo.insert()
- qso
+ contact
end
- defp terrain_attrs(qso) do
+ defp terrain_attrs(contact) do
%{
- qso_id: qso.id,
+ qso_id: contact.id,
sample_count: 65,
path_points: [
%{"lat" => 32.9, "lon" => -97.0, "elev" => 200.0, "dist_km" => 0.0},
@@ -42,18 +42,18 @@ defmodule Microwaveprop.TerrainTest do
describe "upsert_terrain_profile/1" do
test "inserts a new terrain profile" do
- qso = create_qso()
- attrs = terrain_attrs(qso)
+ contact = create_contact()
+ attrs = terrain_attrs(contact)
assert {:ok, profile} = Terrain.upsert_terrain_profile(attrs)
- assert profile.qso_id == qso.id
+ assert profile.qso_id == contact.id
assert profile.sample_count == 65
assert profile.verdict == "CLEAR"
end
test "upserts on conflict (same qso_id)" do
- qso = create_qso()
- attrs = terrain_attrs(qso)
+ contact = create_contact()
+ attrs = terrain_attrs(contact)
{:ok, _} = Terrain.upsert_terrain_profile(attrs)
@@ -67,30 +67,30 @@ defmodule Microwaveprop.TerrainTest do
describe "get_terrain_profile/1" do
test "returns profile for a QSO" do
- qso = create_qso()
- {:ok, _} = Terrain.upsert_terrain_profile(terrain_attrs(qso))
+ contact = create_contact()
+ {:ok, _} = Terrain.upsert_terrain_profile(terrain_attrs(contact))
- profile = Terrain.get_terrain_profile(qso.id)
- assert profile.qso_id == qso.id
+ profile = Terrain.get_terrain_profile(contact.id)
+ assert profile.qso_id == contact.id
end
test "returns nil when no profile exists" do
- qso = create_qso()
- assert Terrain.get_terrain_profile(qso.id) == nil
+ contact = create_contact()
+ assert Terrain.get_terrain_profile(contact.id) == nil
end
end
describe "has_terrain_profile?/1" do
test "returns true when profile exists" do
- qso = create_qso()
- {:ok, _} = Terrain.upsert_terrain_profile(terrain_attrs(qso))
+ contact = create_contact()
+ {:ok, _} = Terrain.upsert_terrain_profile(terrain_attrs(contact))
- assert Terrain.has_terrain_profile?(qso.id)
+ assert Terrain.has_terrain_profile?(contact.id)
end
test "returns false when no profile exists" do
- qso = create_qso()
- refute Terrain.has_terrain_profile?(qso.id)
+ contact = create_contact()
+ refute Terrain.has_terrain_profile?(contact.id)
end
end
end
diff --git a/test/microwaveprop/weather_test.exs b/test/microwaveprop/weather_test.exs
index 84a4ad11..89b5f14f 100644
--- a/test/microwaveprop/weather_test.exs
+++ b/test/microwaveprop/weather_test.exs
@@ -113,7 +113,7 @@ defmodule Microwaveprop.WeatherTest do
end
end
- describe "weather_for_qso/2" do
+ describe "weather_for_contact/2" do
test "returns nearby surface observations within time window with station preloaded" do
{:ok, station} = Weather.find_or_create_station(@station_attrs)
@@ -125,7 +125,7 @@ defmodule Microwaveprop.WeatherTest do
# QSO near the station in space and time
result =
- Weather.weather_for_qso(
+ Weather.weather_for_contact(
%{lat: 32.90, lon: -97.05, timestamp: ~U[2026-03-28 18:30:00Z]},
radius_km: 100,
time_window_hours: 3
@@ -149,7 +149,7 @@ defmodule Microwaveprop.WeatherTest do
})
result =
- Weather.weather_for_qso(
+ Weather.weather_for_contact(
%{lat: 32.85, lon: -97.28, timestamp: ~U[2026-03-28 14:00:00Z]},
radius_km: 100,
time_window_hours: 6
@@ -170,7 +170,7 @@ defmodule Microwaveprop.WeatherTest do
# QSO far away
result =
- Weather.weather_for_qso(
+ Weather.weather_for_contact(
%{lat: 40.0, lon: -80.0, timestamp: ~U[2026-03-28 18:00:00Z]},
radius_km: 100,
time_window_hours: 3
@@ -190,7 +190,7 @@ defmodule Microwaveprop.WeatherTest do
# QSO much later
result =
- Weather.weather_for_qso(
+ Weather.weather_for_contact(
%{lat: 32.90, lon: -97.05, timestamp: ~U[2026-03-28 22:00:00Z]},
radius_km: 100,
time_window_hours: 3
@@ -482,31 +482,31 @@ defmodule Microwaveprop.WeatherTest do
end
end
- describe "hrrr_for_qso/1" do
+ describe "hrrr_for_contact/1" do
test "returns nearest HRRR profile for a QSO" do
Weather.upsert_hrrr_profile(@hrrr_profile_attrs)
- qso = %{
+ contact = %{
pos1: %{"lat" => 32.91, "lon" => -97.05},
qso_timestamp: ~U[2026-03-28 18:30:00Z]
}
- profile = Weather.hrrr_for_qso(qso)
+ profile = Weather.hrrr_for_contact(contact)
assert profile
assert profile.lat == 32.90
end
test "returns nil when no HRRR profiles exist nearby" do
- qso = %{
+ contact = %{
pos1: %{"lat" => 40.0, "lon" => -80.0},
qso_timestamp: ~U[2026-03-28 18:00:00Z]
}
- assert Weather.hrrr_for_qso(qso) == nil
+ assert Weather.hrrr_for_contact(contact) == nil
end
test "returns nil when QSO has no pos1" do
- assert Weather.hrrr_for_qso(%{pos1: nil, qso_timestamp: ~U[2026-03-28 18:00:00Z]}) == nil
+ assert Weather.hrrr_for_contact(%{pos1: nil, qso_timestamp: ~U[2026-03-28 18:00:00Z]}) == nil
end
end
@@ -590,36 +590,36 @@ defmodule Microwaveprop.WeatherTest do
lon: -97.70
})
- qso = %{
+ contact = %{
pos1: %{"lat" => 32.91, "lon" => -97.05},
pos2: %{"lat" => 30.31, "lon" => -97.71},
qso_timestamp: ~U[2026-03-28 18:30:00Z]
}
- profiles = Weather.hrrr_profiles_for_path(qso)
+ profiles = Weather.hrrr_profiles_for_path(contact)
assert length(profiles) >= 2
end
test "returns empty list when no HRRR profiles exist nearby" do
- qso = %{
+ contact = %{
pos1: %{"lat" => 40.0, "lon" => -80.0},
pos2: %{"lat" => 41.0, "lon" => -81.0},
qso_timestamp: ~U[2026-03-28 18:00:00Z]
}
- assert Weather.hrrr_profiles_for_path(qso) == []
+ assert Weather.hrrr_profiles_for_path(contact) == []
end
test "returns profiles for pos1 only when pos2 is nil" do
Weather.upsert_hrrr_profile(@hrrr_profile_attrs)
- qso = %{
+ contact = %{
pos1: %{"lat" => 32.91, "lon" => -97.05},
pos2: nil,
qso_timestamp: ~U[2026-03-28 18:30:00Z]
}
- profiles = Weather.hrrr_profiles_for_path(qso)
+ profiles = Weather.hrrr_profiles_for_path(contact)
assert length(profiles) == 1
end
end
@@ -636,36 +636,36 @@ defmodule Microwaveprop.WeatherTest do
hourly: [%{"hour" => 0, "p01m_mm" => 1.0}]
})
- qso = %{
+ contact = %{
pos1: %{"lat" => 32.9, "lon" => -97.0},
pos2: %{"lat" => 30.3, "lon" => -97.7},
qso_timestamp: ~U[2026-03-28 18:00:00Z]
}
- observations = Weather.iemre_for_path(qso)
+ observations = Weather.iemre_for_path(contact)
assert length(observations) >= 2
end
test "returns empty list when no observations exist" do
- qso = %{
+ contact = %{
pos1: %{"lat" => 40.0, "lon" => -80.0},
pos2: %{"lat" => 41.0, "lon" => -81.0},
qso_timestamp: ~U[2026-03-28 18:00:00Z]
}
- assert Weather.iemre_for_path(qso) == []
+ assert Weather.iemre_for_path(contact) == []
end
test "returns observation for pos1 only when pos2 is nil" do
Weather.upsert_iemre_observation(@iemre_attrs)
- qso = %{
+ contact = %{
pos1: %{"lat" => 32.9, "lon" => -97.0},
pos2: nil,
qso_timestamp: ~U[2026-03-28 18:00:00Z]
}
- observations = Weather.iemre_for_path(qso)
+ observations = Weather.iemre_for_path(contact)
assert length(observations) == 1
end
end
@@ -684,31 +684,31 @@ defmodule Microwaveprop.WeatherTest do
end
end
- describe "iemre_for_qso/1" do
+ describe "iemre_for_contact/1" do
test "returns IEMRE observation matching QSO pos1 and date" do
Weather.upsert_iemre_observation(@iemre_attrs)
- qso = %{
+ contact = %{
pos1: %{"lat" => 32.9, "lon" => -97.0},
qso_timestamp: ~U[2026-03-28 18:00:00Z]
}
- obs = Weather.iemre_for_qso(qso)
+ obs = Weather.iemre_for_contact(contact)
assert obs
assert obs.lat == 32.875
end
test "returns nil when no IEMRE observation exists" do
- qso = %{
+ contact = %{
pos1: %{"lat" => 40.0, "lon" => -80.0},
qso_timestamp: ~U[2026-03-28 18:00:00Z]
}
- assert Weather.iemre_for_qso(qso) == nil
+ assert Weather.iemre_for_contact(contact) == nil
end
test "returns nil when QSO has no pos1" do
- assert Weather.iemre_for_qso(%{pos1: nil, qso_timestamp: ~U[2026-03-28 18:00:00Z]}) == nil
+ assert Weather.iemre_for_contact(%{pos1: nil, qso_timestamp: ~U[2026-03-28 18:00:00Z]}) == nil
end
end
end
diff --git a/test/microwaveprop/workers/contact_weather_enqueue_worker_test.exs b/test/microwaveprop/workers/contact_weather_enqueue_worker_test.exs
index d11969d1..debee52e 100644
--- a/test/microwaveprop/workers/contact_weather_enqueue_worker_test.exs
+++ b/test/microwaveprop/workers/contact_weather_enqueue_worker_test.exs
@@ -1,12 +1,12 @@
-defmodule Microwaveprop.Workers.QsoWeatherEnqueueWorkerTest do
+defmodule Microwaveprop.Workers.ContactWeatherEnqueueWorkerTest do
use Microwaveprop.DataCase, async: true
- alias Microwaveprop.Radio.Qso
+ alias Microwaveprop.Radio.Contact
alias Microwaveprop.Terrain.ElevationClient
alias Microwaveprop.Weather
alias Microwaveprop.Weather.HrrrClient
alias Microwaveprop.Weather.IemClient
- alias Microwaveprop.Workers.QsoWeatherEnqueueWorker
+ alias Microwaveprop.Workers.ContactWeatherEnqueueWorker
@qso_attrs %{
station1: "W5XD",
@@ -21,13 +21,13 @@ defmodule Microwaveprop.Workers.QsoWeatherEnqueueWorkerTest do
distance_km: Decimal.new("295")
}
- defp create_qso(attrs \\ %{}) do
- {:ok, qso} =
- %Qso{}
- |> Qso.changeset(Map.merge(@qso_attrs, attrs))
+ defp create_contact(attrs \\ %{}) do
+ {:ok, contact} =
+ %Contact{}
+ |> Contact.changeset(Map.merge(@qso_attrs, attrs))
|> Repo.insert()
- qso
+ contact
end
defp create_asos_station(code, lat, lon) do
@@ -59,9 +59,9 @@ defmodule Microwaveprop.Workers.QsoWeatherEnqueueWorkerTest do
describe "build_weather_jobs/1" do
test "builds ASOS jobs for nearby stations" do
station = create_asos_station("KDFW", 32.90, -97.04)
- qso = create_qso()
+ contact = create_contact()
- jobs = QsoWeatherEnqueueWorker.build_weather_jobs([qso])
+ jobs = ContactWeatherEnqueueWorker.build_weather_jobs([contact])
asos_jobs =
Enum.filter(jobs, fn j ->
@@ -79,9 +79,9 @@ defmodule Microwaveprop.Workers.QsoWeatherEnqueueWorkerTest do
test "builds RAOB jobs for nearby sounding stations" do
station = create_sounding_station("FWD", 32.83, -97.30)
- qso = create_qso()
+ contact = create_contact()
- jobs = QsoWeatherEnqueueWorker.build_weather_jobs([qso])
+ jobs = ContactWeatherEnqueueWorker.build_weather_jobs([contact])
raob_jobs =
Enum.filter(jobs, fn j ->
@@ -100,10 +100,10 @@ defmodule Microwaveprop.Workers.QsoWeatherEnqueueWorkerTest do
_station = create_asos_station("KDFW", 32.90, -97.04)
# Two QSOs at the exact same time produce identical args → deduplicated
- q1 = create_qso(%{station1: "A1", qso_timestamp: ~U[2026-03-28 18:00:00Z]})
- q2 = create_qso(%{station1: "A2", qso_timestamp: ~U[2026-03-28 18:00:00Z]})
+ q1 = create_contact(%{station1: "A1", qso_timestamp: ~U[2026-03-28 18:00:00Z]})
+ q2 = create_contact(%{station1: "A2", qso_timestamp: ~U[2026-03-28 18:00:00Z]})
- jobs = QsoWeatherEnqueueWorker.build_weather_jobs([q1, q2])
+ jobs = ContactWeatherEnqueueWorker.build_weather_jobs([q1, q2])
asos_jobs =
Enum.filter(jobs, fn j ->
@@ -116,31 +116,31 @@ defmodule Microwaveprop.Workers.QsoWeatherEnqueueWorkerTest do
test "builds ASOS jobs for stations near pos2 and midpoint" do
# Station near pos2 (30.3, -97.7) but NOT near pos1 (32.9, -97.0)
station_near_pos2 = create_asos_station("KAUS", 30.20, -97.68)
- qso = create_qso()
+ contact = create_contact()
- jobs = QsoWeatherEnqueueWorker.build_weather_jobs([qso])
+ jobs = ContactWeatherEnqueueWorker.build_weather_jobs([contact])
station_ids = Enum.map(jobs, & &1.changes.args["station_id"])
assert station_near_pos2.id in station_ids
end
test "returns empty list when no stations nearby" do
- qso = create_qso(%{pos1: %{"lat" => 60.0, "lon" => -150.0}, pos2: nil})
+ contact = create_contact(%{pos1: %{"lat" => 60.0, "lon" => -150.0}, pos2: nil})
- assert QsoWeatherEnqueueWorker.build_weather_jobs([qso]) == []
+ assert ContactWeatherEnqueueWorker.build_weather_jobs([contact]) == []
end
test "skips ASOS job when surface observations already exist for that station and time window" do
station = create_asos_station("KDFW", 32.90, -97.04)
- qso = create_qso()
+ contact = create_contact()
- # Insert a surface observation within the time window (qso ± 2 hours)
+ # Insert a surface observation within the time window (contact ± 2 hours)
Weather.upsert_surface_observation(station, %{
observed_at: ~U[2026-03-28 18:00:00Z],
temp_f: 75.0
})
- jobs = QsoWeatherEnqueueWorker.build_weather_jobs([qso])
+ jobs = ContactWeatherEnqueueWorker.build_weather_jobs([contact])
asos_jobs =
Enum.filter(jobs, fn j ->
@@ -152,10 +152,10 @@ defmodule Microwaveprop.Workers.QsoWeatherEnqueueWorkerTest do
test "skips RAOB job when sounding already exists for that station and time" do
station = create_sounding_station("FWD", 32.83, -97.30)
- qso = create_qso()
+ contact = create_contact()
# Insert soundings for both bracketing times (00Z and 12Z on 2026-03-28)
- for time <- Weather.sounding_times_around(qso.qso_timestamp) do
+ for time <- Weather.sounding_times_around(contact.qso_timestamp) do
Weather.upsert_sounding(station, %{
observed_at: time,
profile: [%{"pres" => 1013.0, "tmpc" => 25.0}],
@@ -163,7 +163,7 @@ defmodule Microwaveprop.Workers.QsoWeatherEnqueueWorkerTest do
})
end
- jobs = QsoWeatherEnqueueWorker.build_weather_jobs([qso])
+ jobs = ContactWeatherEnqueueWorker.build_weather_jobs([contact])
raob_jobs =
Enum.filter(jobs, fn j ->
@@ -207,58 +207,58 @@ defmodule Microwaveprop.Workers.QsoWeatherEnqueueWorkerTest do
test "enqueues weather jobs and marks QSOs as queued" do
_station = create_asos_station("KDFW", 32.90, -97.04)
- qso = create_qso()
+ contact = create_contact()
- assert qso.weather_queued == false
+ assert contact.weather_queued == false
- assert :ok = QsoWeatherEnqueueWorker.perform(%Oban.Job{args: %{}})
+ assert :ok = ContactWeatherEnqueueWorker.perform(%Oban.Job{args: %{}})
- updated = Repo.get!(Qso, qso.id)
+ updated = Repo.get!(Contact, contact.id)
assert updated.weather_queued == true
end
test "returns :ok when no unprocessed QSOs exist" do
- assert :ok = QsoWeatherEnqueueWorker.perform(%Oban.Job{args: %{}})
+ assert :ok = ContactWeatherEnqueueWorker.perform(%Oban.Job{args: %{}})
end
test "backfills distance_km for QSOs missing it" do
_station = create_asos_station("KDFW", 32.90, -97.04)
- qso = create_qso(%{distance_km: nil})
+ contact = create_contact(%{distance_km: nil})
- assert :ok = QsoWeatherEnqueueWorker.perform(%Oban.Job{args: %{}})
+ assert :ok = ContactWeatherEnqueueWorker.perform(%Oban.Job{args: %{}})
- updated = Repo.get!(Qso, qso.id)
+ updated = Repo.get!(Contact, contact.id)
assert updated.distance_km
assert_in_delta Decimal.to_float(updated.distance_km), 295, 5
end
test "skips QSOs without positions" do
_station = create_asos_station("KDFW", 32.90, -97.04)
- _qso = create_qso(%{pos1: nil})
+ _qso = create_contact(%{pos1: nil})
- assert :ok = QsoWeatherEnqueueWorker.perform(%Oban.Job{args: %{}})
+ assert :ok = ContactWeatherEnqueueWorker.perform(%Oban.Job{args: %{}})
# QSO without pos1 should not be marked as queued
- all_qsos = Repo.all(Qso)
+ all_qsos = Repo.all(Contact)
refute Enum.any?(all_qsos, & &1.weather_queued)
end
test "enqueues HRRR jobs and marks QSOs as hrrr_queued" do
- qso = create_qso()
- assert qso.hrrr_queued == false
+ contact = create_contact()
+ assert contact.hrrr_queued == false
- assert :ok = QsoWeatherEnqueueWorker.perform(%Oban.Job{args: %{}})
+ assert :ok = ContactWeatherEnqueueWorker.perform(%Oban.Job{args: %{}})
- updated = Repo.get!(Qso, qso.id)
+ updated = Repo.get!(Contact, contact.id)
assert updated.hrrr_queued == true
end
end
describe "build_hrrr_jobs/1" do
test "batches all path points into one job per HRRR hour" do
- qso = create_qso(%{pos1: %{"lat" => 32.907, "lon" => -97.038}})
+ contact = create_contact(%{pos1: %{"lat" => 32.907, "lon" => -97.038}})
- jobs = QsoWeatherEnqueueWorker.build_hrrr_jobs([qso])
+ jobs = ContactWeatherEnqueueWorker.build_hrrr_jobs([contact])
# All 3 path points share the same HRRR hour → 1 batch job
assert length(jobs) == 1
@@ -271,9 +271,9 @@ defmodule Microwaveprop.Workers.QsoWeatherEnqueueWorkerTest do
end
test "batch contains single point when pos2 is nil" do
- qso = create_qso(%{pos1: %{"lat" => 32.907, "lon" => -97.038}, pos2: nil})
+ contact = create_contact(%{pos1: %{"lat" => 32.907, "lon" => -97.038}, pos2: nil})
- jobs = QsoWeatherEnqueueWorker.build_hrrr_jobs([qso])
+ jobs = ContactWeatherEnqueueWorker.build_hrrr_jobs([contact])
assert length(jobs) == 1
job = hd(jobs)
@@ -283,9 +283,9 @@ defmodule Microwaveprop.Workers.QsoWeatherEnqueueWorkerTest do
end
test "rounds valid_time to nearest hour" do
- qso = create_qso(%{qso_timestamp: ~U[2026-03-28 18:45:00Z], pos2: nil})
+ contact = create_contact(%{qso_timestamp: ~U[2026-03-28 18:45:00Z], pos2: nil})
- jobs = QsoWeatherEnqueueWorker.build_hrrr_jobs([qso])
+ jobs = ContactWeatherEnqueueWorker.build_hrrr_jobs([contact])
job = hd(jobs)
assert job.changes.args["valid_time"] == "2026-03-28T19:00:00Z"
@@ -293,7 +293,7 @@ defmodule Microwaveprop.Workers.QsoWeatherEnqueueWorkerTest do
test "deduplicates jobs across QSOs with identical path points" do
q1 =
- create_qso(%{
+ create_contact(%{
station1: "A1",
qso_timestamp: ~U[2026-03-28 18:10:00Z],
pos1: %{"lat" => 32.90, "lon" => -97.04},
@@ -301,14 +301,14 @@ defmodule Microwaveprop.Workers.QsoWeatherEnqueueWorkerTest do
})
q2 =
- create_qso(%{
+ create_contact(%{
station1: "A2",
qso_timestamp: ~U[2026-03-28 18:20:00Z],
pos1: %{"lat" => 32.90, "lon" => -97.04},
pos2: nil
})
- jobs = QsoWeatherEnqueueWorker.build_hrrr_jobs([q1, q2])
+ jobs = ContactWeatherEnqueueWorker.build_hrrr_jobs([q1, q2])
# Both round to same hour and same location → 1 job
assert length(jobs) == 1
@@ -316,26 +316,26 @@ defmodule Microwaveprop.Workers.QsoWeatherEnqueueWorkerTest do
test "deduplicates path points that round to the same HRRR grid cell" do
# pos1 and pos2 only 0.001° apart — all 3 points round to same HRRR cell
- qso =
- create_qso(%{
+ contact =
+ create_contact(%{
pos1: %{"lat" => 32.901, "lon" => -97.041},
pos2: %{"lat" => 32.902, "lon" => -97.042}
})
- jobs = QsoWeatherEnqueueWorker.build_hrrr_jobs([qso])
+ jobs = ContactWeatherEnqueueWorker.build_hrrr_jobs([contact])
# All 3 path points round to {32.90, -97.04} → 1 job
assert length(jobs) == 1
end
test "skips QSOs without pos1" do
- qso = create_qso(%{pos1: nil})
+ contact = create_contact(%{pos1: nil})
- assert QsoWeatherEnqueueWorker.build_hrrr_jobs([qso]) == []
+ assert ContactWeatherEnqueueWorker.build_hrrr_jobs([contact]) == []
end
test "excludes existing points from batch" do
- qso = create_qso(%{pos1: %{"lat" => 32.907, "lon" => -97.038}})
+ contact = create_contact(%{pos1: %{"lat" => 32.907, "lon" => -97.038}})
# Insert a profile at the pos1 rounded grid point only
Weather.upsert_hrrr_profile(%{
@@ -351,7 +351,7 @@ defmodule Microwaveprop.Workers.QsoWeatherEnqueueWorkerTest do
surface_pressure_mb: 1013.0
})
- jobs = QsoWeatherEnqueueWorker.build_hrrr_jobs([qso])
+ jobs = ContactWeatherEnqueueWorker.build_hrrr_jobs([contact])
# 1 batch job, but pos1 excluded from points list
assert length(jobs) == 1
@@ -364,19 +364,19 @@ defmodule Microwaveprop.Workers.QsoWeatherEnqueueWorkerTest do
describe "build_terrain_jobs/1" do
test "builds one terrain job per QSO" do
- qso = create_qso()
+ contact = create_contact()
- jobs = QsoWeatherEnqueueWorker.build_terrain_jobs([qso])
+ jobs = ContactWeatherEnqueueWorker.build_terrain_jobs([contact])
assert length(jobs) == 1
job = hd(jobs)
- assert job.changes.args["qso_id"] == qso.id
+ assert job.changes.args["qso_id"] == contact.id
end
test "deduplicates by qso_id" do
- qso = create_qso()
+ contact = create_contact()
- jobs = QsoWeatherEnqueueWorker.build_terrain_jobs([qso, qso])
+ jobs = ContactWeatherEnqueueWorker.build_terrain_jobs([contact, contact])
assert length(jobs) == 1
end
end
@@ -410,30 +410,30 @@ defmodule Microwaveprop.Workers.QsoWeatherEnqueueWorkerTest do
end
test "enqueues terrain jobs and marks QSOs as terrain_queued" do
- qso = create_qso()
- assert qso.terrain_queued == false
+ contact = create_contact()
+ assert contact.terrain_queued == false
- assert :ok = QsoWeatherEnqueueWorker.perform(%Oban.Job{args: %{}})
+ assert :ok = ContactWeatherEnqueueWorker.perform(%Oban.Job{args: %{}})
- updated = Repo.get!(Qso, qso.id)
+ updated = Repo.get!(Contact, contact.id)
assert updated.terrain_queued == true
end
test "skips terrain for QSOs missing pos2" do
- _qso = create_qso(%{pos2: nil})
+ _qso = create_contact(%{pos2: nil})
- assert :ok = QsoWeatherEnqueueWorker.perform(%Oban.Job{args: %{}})
+ assert :ok = ContactWeatherEnqueueWorker.perform(%Oban.Job{args: %{}})
- all_qsos = Repo.all(Qso)
+ all_qsos = Repo.all(Contact)
refute Enum.any?(all_qsos, & &1.terrain_queued)
end
end
describe "build_iemre_jobs/1" do
test "builds IEMRE jobs for all path points (pos1, midpoint, pos2)" do
- qso = create_qso(%{pos1: %{"lat" => 32.907, "lon" => -97.038}})
+ contact = create_contact(%{pos1: %{"lat" => 32.907, "lon" => -97.038}})
- jobs = QsoWeatherEnqueueWorker.build_iemre_jobs([qso])
+ jobs = ContactWeatherEnqueueWorker.build_iemre_jobs([contact])
# pos1, midpoint, pos2 may have distinct IEMRE grid cells
assert length(jobs) >= 2
@@ -442,9 +442,9 @@ defmodule Microwaveprop.Workers.QsoWeatherEnqueueWorkerTest do
end
test "builds only one IEMRE job when pos2 is nil" do
- qso = create_qso(%{pos1: %{"lat" => 32.907, "lon" => -97.038}, pos2: nil})
+ contact = create_contact(%{pos1: %{"lat" => 32.907, "lon" => -97.038}, pos2: nil})
- jobs = QsoWeatherEnqueueWorker.build_iemre_jobs([qso])
+ jobs = ContactWeatherEnqueueWorker.build_iemre_jobs([contact])
assert length(jobs) == 1
job = hd(jobs)
@@ -455,7 +455,7 @@ defmodule Microwaveprop.Workers.QsoWeatherEnqueueWorkerTest do
test "deduplicates jobs across QSOs with identical path points" do
q1 =
- create_qso(%{
+ create_contact(%{
station1: "A1",
qso_timestamp: ~U[2026-03-28 18:10:00Z],
pos1: %{"lat" => 32.90, "lon" => -97.04},
@@ -463,14 +463,14 @@ defmodule Microwaveprop.Workers.QsoWeatherEnqueueWorkerTest do
})
q2 =
- create_qso(%{
+ create_contact(%{
station1: "A2",
qso_timestamp: ~U[2026-03-28 20:20:00Z],
pos1: %{"lat" => 32.90, "lon" => -97.04},
pos2: nil
})
- jobs = QsoWeatherEnqueueWorker.build_iemre_jobs([q1, q2])
+ jobs = ContactWeatherEnqueueWorker.build_iemre_jobs([q1, q2])
# Same location and same date → 1 job
assert length(jobs) == 1
@@ -478,7 +478,7 @@ defmodule Microwaveprop.Workers.QsoWeatherEnqueueWorkerTest do
test "creates separate jobs for different dates" do
q1 =
- create_qso(%{
+ create_contact(%{
station1: "A1",
qso_timestamp: ~U[2026-03-28 23:00:00Z],
pos1: %{"lat" => 32.90, "lon" => -97.04},
@@ -486,24 +486,24 @@ defmodule Microwaveprop.Workers.QsoWeatherEnqueueWorkerTest do
})
q2 =
- create_qso(%{
+ create_contact(%{
station1: "A2",
qso_timestamp: ~U[2026-03-29 01:00:00Z],
pos1: %{"lat" => 32.90, "lon" => -97.04},
pos2: nil
})
- jobs = QsoWeatherEnqueueWorker.build_iemre_jobs([q1, q2])
+ jobs = ContactWeatherEnqueueWorker.build_iemre_jobs([q1, q2])
assert length(jobs) == 2
end
test "skips QSOs without pos1" do
- qso = create_qso(%{pos1: nil})
- assert QsoWeatherEnqueueWorker.build_iemre_jobs([qso]) == []
+ contact = create_contact(%{pos1: nil})
+ assert ContactWeatherEnqueueWorker.build_iemre_jobs([contact]) == []
end
test "skips only path points where IEMRE observation already exists" do
- qso = create_qso(%{pos1: %{"lat" => 32.907, "lon" => -97.038}})
+ contact = create_contact(%{pos1: %{"lat" => 32.907, "lon" => -97.038}})
# Insert an IEMRE observation at the pos1 rounded grid point only
Weather.upsert_iemre_observation(%{
@@ -513,7 +513,7 @@ defmodule Microwaveprop.Workers.QsoWeatherEnqueueWorkerTest do
hourly: [%{"hour" => 0, "p01m_mm" => 0.0}]
})
- jobs = QsoWeatherEnqueueWorker.build_iemre_jobs([qso])
+ jobs = ContactWeatherEnqueueWorker.build_iemre_jobs([contact])
# pos1 skipped, midpoint + pos2 still need fetching
assert length(jobs) >= 1
@@ -551,17 +551,17 @@ defmodule Microwaveprop.Workers.QsoWeatherEnqueueWorkerTest do
end
test "enqueues IEMRE jobs and marks QSOs as iemre_queued" do
- qso = create_qso()
- assert qso.iemre_queued == false
+ contact = create_contact()
+ assert contact.iemre_queued == false
- assert :ok = QsoWeatherEnqueueWorker.perform(%Oban.Job{args: %{}})
+ assert :ok = ContactWeatherEnqueueWorker.perform(%Oban.Job{args: %{}})
- updated = Repo.get!(Qso, qso.id)
+ updated = Repo.get!(Contact, contact.id)
assert updated.iemre_queued == true
end
end
- describe "enqueue_for_qso/1" do
+ describe "enqueue_for_contact/1" do
setup do
Req.Test.stub(IemClient, fn conn ->
case conn.request_path do
@@ -591,16 +591,16 @@ defmodule Microwaveprop.Workers.QsoWeatherEnqueueWorkerTest do
test "enqueues all enrichment jobs and marks QSO as fully queued" do
_station = create_asos_station("KDFW", 32.90, -97.04)
- qso = create_qso()
+ contact = create_contact()
- assert qso.weather_queued == false
- assert qso.hrrr_queued == false
- assert qso.terrain_queued == false
- assert qso.iemre_queued == false
+ assert contact.weather_queued == false
+ assert contact.hrrr_queued == false
+ assert contact.terrain_queued == false
+ assert contact.iemre_queued == false
- assert :ok = QsoWeatherEnqueueWorker.enqueue_for_qso(qso)
+ assert :ok = ContactWeatherEnqueueWorker.enqueue_for_contact(contact)
- updated = Repo.get!(Qso, qso.id)
+ updated = Repo.get!(Contact, contact.id)
assert updated.weather_queued == true
assert updated.hrrr_queued == true
assert updated.terrain_queued == true
@@ -609,11 +609,11 @@ defmodule Microwaveprop.Workers.QsoWeatherEnqueueWorkerTest do
test "works for QSO without pos2 (marks terrain_queued false)" do
_station = create_asos_station("KDFW", 32.90, -97.04)
- qso = create_qso(%{pos2: nil})
+ contact = create_contact(%{pos2: nil})
- assert :ok = QsoWeatherEnqueueWorker.enqueue_for_qso(qso)
+ assert :ok = ContactWeatherEnqueueWorker.enqueue_for_contact(contact)
- updated = Repo.get!(Qso, qso.id)
+ updated = Repo.get!(Contact, contact.id)
assert updated.weather_queued == true
assert updated.hrrr_queued == true
# terrain requires pos2, so it can't be queued
diff --git a/test/microwaveprop/workers/terrain_profile_worker_test.exs b/test/microwaveprop/workers/terrain_profile_worker_test.exs
index 77f7844b..68c1a768 100644
--- a/test/microwaveprop/workers/terrain_profile_worker_test.exs
+++ b/test/microwaveprop/workers/terrain_profile_worker_test.exs
@@ -1,7 +1,7 @@
defmodule Microwaveprop.Workers.TerrainProfileWorkerTest do
use Microwaveprop.DataCase, async: true
- alias Microwaveprop.Radio.Qso
+ alias Microwaveprop.Radio.Contact
alias Microwaveprop.Terrain
alias Microwaveprop.Terrain.ElevationClient
alias Microwaveprop.Workers.TerrainProfileWorker
@@ -17,13 +17,13 @@ defmodule Microwaveprop.Workers.TerrainProfileWorkerTest do
distance_km: Decimal.new("295")
}
- defp create_qso(attrs \\ %{}) do
- {:ok, qso} =
- %Qso{}
- |> Qso.changeset(Map.merge(@qso_attrs, attrs))
+ defp create_contact(attrs \\ %{}) do
+ {:ok, contact} =
+ %Contact{}
+ |> Contact.changeset(Map.merge(@qso_attrs, attrs))
|> Repo.insert()
- qso
+ contact
end
defp stub_elevation_api do
@@ -38,15 +38,15 @@ defmodule Microwaveprop.Workers.TerrainProfileWorkerTest do
describe "perform/1" do
test "fetches elevation and stores terrain profile" do
stub_elevation_api()
- qso = create_qso()
+ contact = create_contact()
- refute Terrain.has_terrain_profile?(qso.id)
+ refute Terrain.has_terrain_profile?(contact.id)
- job = TerrainProfileWorker.new(%{"qso_id" => qso.id})
+ job = TerrainProfileWorker.new(%{"qso_id" => contact.id})
assert :ok = TerrainProfileWorker.perform(%Oban.Job{args: job.changes.args})
- assert Terrain.has_terrain_profile?(qso.id)
- profile = Terrain.get_terrain_profile(qso.id)
+ assert Terrain.has_terrain_profile?(contact.id)
+ profile = Terrain.get_terrain_profile(contact.id)
assert profile.sample_count == 65
assert profile.verdict in ["CLEAR", "FRESNEL_MINOR", "FRESNEL_PARTIAL", "BLOCKED"]
assert is_list(profile.path_points)
@@ -54,17 +54,17 @@ defmodule Microwaveprop.Workers.TerrainProfileWorkerTest do
test "skips if terrain profile already exists" do
stub_elevation_api()
- qso = create_qso()
+ contact = create_contact()
{:ok, _} =
Terrain.upsert_terrain_profile(%{
- qso_id: qso.id,
+ qso_id: contact.id,
sample_count: 65,
path_points: [],
verdict: "CLEAR"
})
- job = TerrainProfileWorker.new(%{"qso_id" => qso.id})
+ job = TerrainProfileWorker.new(%{"qso_id" => contact.id})
assert :ok = TerrainProfileWorker.perform(%Oban.Job{args: job.changes.args})
end
@@ -73,22 +73,22 @@ defmodule Microwaveprop.Workers.TerrainProfileWorkerTest do
Plug.Conn.send_resp(conn, 500, "error")
end)
- qso = create_qso()
+ contact = create_contact()
- job = TerrainProfileWorker.new(%{"qso_id" => qso.id})
+ job = TerrainProfileWorker.new(%{"qso_id" => contact.id})
assert {:error, _} = TerrainProfileWorker.perform(%Oban.Job{args: job.changes.args})
- refute Terrain.has_terrain_profile?(qso.id)
+ refute Terrain.has_terrain_profile?(contact.id)
end
test "uses correct frequency from band" do
stub_elevation_api()
- qso = create_qso(%{band: Decimal.new("10368")})
+ contact = create_contact(%{band: Decimal.new("10368")})
- job = TerrainProfileWorker.new(%{"qso_id" => qso.id})
+ job = TerrainProfileWorker.new(%{"qso_id" => contact.id})
assert :ok = TerrainProfileWorker.perform(%Oban.Job{args: job.changes.args})
- profile = Terrain.get_terrain_profile(qso.id)
+ profile = Terrain.get_terrain_profile(contact.id)
assert profile.verdict
end
end
diff --git a/test/microwaveprop_web/live/contact_live_test.exs b/test/microwaveprop_web/live/contact_live_test.exs
index 1a8e8a24..37b14641 100644
--- a/test/microwaveprop_web/live/contact_live_test.exs
+++ b/test/microwaveprop_web/live/contact_live_test.exs
@@ -1,12 +1,12 @@
-defmodule MicrowavepropWeb.QsoLiveTest do
+defmodule MicrowavepropWeb.ContactLiveTest do
use MicrowavepropWeb.ConnCase, async: true
import Phoenix.LiveViewTest
- alias Microwaveprop.Radio.Qso
+ alias Microwaveprop.Radio.Contact
alias Microwaveprop.Repo
- defp create_qso(attrs \\ %{}) do
+ defp create_contact(attrs \\ %{}) do
default = %{
station1: "W5XD",
station2: "K5TR",
@@ -20,24 +20,24 @@ defmodule MicrowavepropWeb.QsoLiveTest do
distance_km: Decimal.new("295")
}
- {:ok, qso} =
- %Qso{}
- |> Qso.changeset(Map.merge(default, attrs))
+ {:ok, contact} =
+ %Contact{}
+ |> Contact.changeset(Map.merge(default, attrs))
|> Repo.insert()
- qso
+ contact
end
describe "Index" do
test "renders page with QSOs heading", %{conn: conn} do
- {:ok, _lv, html} = live(conn, ~p"/qsos")
+ {:ok, _lv, html} = live(conn, ~p"/contacts")
assert html =~ "QSOs"
end
test "table shows QSO data", %{conn: conn} do
- create_qso()
+ create_contact()
- {:ok, _lv, html} = live(conn, ~p"/qsos")
+ {:ok, _lv, html} = live(conn, ~p"/contacts")
assert html =~ "W5XD"
assert html =~ "K5TR"
assert html =~ "CW"
@@ -45,17 +45,17 @@ defmodule MicrowavepropWeb.QsoLiveTest do
end
test "row links to detail page", %{conn: conn} do
- qso = create_qso()
+ contact = create_contact()
- {:ok, _lv, html} = live(conn, ~p"/qsos")
- assert html =~ ~p"/qsos/#{qso.id}"
+ {:ok, _lv, html} = live(conn, ~p"/contacts")
+ assert html =~ ~p"/contacts/#{contact.id}"
end
test "sort params order the table", %{conn: conn} do
- create_qso(%{station1: "ZZ9ZZ"})
- create_qso(%{station1: "AA1AA"})
+ create_contact(%{station1: "ZZ9ZZ"})
+ create_contact(%{station1: "AA1AA"})
- {:ok, _lv, html} = live(conn, ~p"/qsos?sort_by=station1&sort_order=asc")
+ {:ok, _lv, html} = live(conn, ~p"/contacts?sort_by=station1&sort_order=asc")
# AA1AA should appear before ZZ9ZZ
aa_pos = html |> :binary.match("AA1AA") |> elem(0)
@@ -66,22 +66,22 @@ defmodule MicrowavepropWeb.QsoLiveTest do
test "pagination works", %{conn: conn} do
for i <- 1..25 do
ts = DateTime.add(~U[2026-01-01 00:00:00Z], i * 3600, :second)
- create_qso(%{qso_timestamp: ts})
+ create_contact(%{qso_timestamp: ts})
end
- {:ok, _lv, html} = live(conn, ~p"/qsos")
+ {:ok, _lv, html} = live(conn, ~p"/contacts")
assert html =~ "Page 1 of 2"
- {:ok, _lv, html2} = live(conn, ~p"/qsos?page=2")
+ {:ok, _lv, html2} = live(conn, ~p"/contacts?page=2")
assert html2 =~ "Page 2 of 2"
end
end
describe "Show" do
test "renders QSO detail fields", %{conn: conn} do
- qso = create_qso()
+ contact = create_contact()
- {:ok, _lv, html} = live(conn, ~p"/qsos/#{qso.id}")
+ {:ok, _lv, html} = live(conn, ~p"/contacts/#{contact.id}")
assert html =~ "W5XD"
assert html =~ "K5TR"
assert html =~ "CW"
@@ -91,38 +91,38 @@ defmodule MicrowavepropWeb.QsoLiveTest do
end
test "shows surface observations section", %{conn: conn} do
- qso = create_qso()
- {:ok, _lv, html} = live(conn, ~p"/qsos/#{qso.id}")
+ contact = create_contact()
+ {:ok, _lv, html} = live(conn, ~p"/contacts/#{contact.id}")
assert html =~ "Surface Observations"
end
test "shows sounding section", %{conn: conn} do
- qso = create_qso()
- {:ok, _lv, html} = live(conn, ~p"/qsos/#{qso.id}")
+ contact = create_contact()
+ {:ok, _lv, html} = live(conn, ~p"/contacts/#{contact.id}")
assert html =~ "Soundings"
end
test "shows solar index section", %{conn: conn} do
- qso = create_qso()
- {:ok, _lv, html} = live(conn, ~p"/qsos/#{qso.id}")
+ contact = create_contact()
+ {:ok, _lv, html} = live(conn, ~p"/contacts/#{contact.id}")
assert html =~ "Solar Conditions"
end
test "renders with sort assigns", %{conn: conn} do
- qso = create_qso()
- {:ok, lv, html} = live(conn, ~p"/qsos/#{qso.id}")
+ contact = create_contact()
+ {:ok, lv, html} = live(conn, ~p"/contacts/#{contact.id}")
# Page renders without error
assert html =~ "Surface Observations"
assert html =~ "Soundings"
# Sort assigns are initialized (no crash on render)
- assert render(lv) =~ qso.station1
+ assert render(lv) =~ contact.station1
end
test "raises for bad UUID", %{conn: conn} do
assert_raise Ecto.NoResultsError, fn ->
- live(conn, ~p"/qsos/#{Ecto.UUID.generate()}")
+ live(conn, ~p"/contacts/#{Ecto.UUID.generate()}")
end
end
end
diff --git a/test/microwaveprop_web/live/submit_live_test.exs b/test/microwaveprop_web/live/submit_live_test.exs
index 97868917..edb4acc1 100644
--- a/test/microwaveprop_web/live/submit_live_test.exs
+++ b/test/microwaveprop_web/live/submit_live_test.exs
@@ -3,7 +3,7 @@ defmodule MicrowavepropWeb.SubmitLiveTest do
import Phoenix.LiveViewTest
- alias Microwaveprop.Radio.Qso
+ alias Microwaveprop.Radio.Contact
alias Microwaveprop.Repo
alias Microwaveprop.Terrain.ElevationClient
alias Microwaveprop.Weather.HrrrClient
@@ -33,7 +33,7 @@ defmodule MicrowavepropWeb.SubmitLiveTest do
describe "mount" do
test "renders the submission form", %{conn: conn} do
{:ok, _lv, html} = live(conn, ~p"/submit")
- assert html =~ "Submit QSO"
+ assert html =~ "Submit Contact"
assert html =~ "Station 1"
assert html =~ "Station 2"
assert html =~ "Grid 1"
@@ -49,7 +49,7 @@ defmodule MicrowavepropWeb.SubmitLiveTest do
html =
lv
- |> form("#qso-form", qso: %{station1: ""})
+ |> form("#contact-form", contact: %{station1: ""})
|> render_change()
assert html =~ "can't be blank"
@@ -61,8 +61,8 @@ defmodule MicrowavepropWeb.SubmitLiveTest do
{:ok, lv, _html} = live(conn, ~p"/submit")
lv
- |> form("#qso-form",
- qso: %{
+ |> form("#contact-form",
+ contact: %{
station1: "W5XD",
station2: "K5TR",
qso_timestamp: "2026-03-28T18:00",
@@ -75,12 +75,12 @@ defmodule MicrowavepropWeb.SubmitLiveTest do
)
|> render_submit()
- qso = Repo.one!(Qso)
- assert qso.station1 == "W5XD"
- assert qso.user_submitted == true
- assert qso.pos1["lat"]
+ contact = Repo.one!(Contact)
+ assert contact.station1 == "W5XD"
+ assert contact.user_submitted == true
+ assert contact.pos1["lat"]
- assert_redirect(lv, ~p"/qsos/#{qso.id}")
+ assert_redirect(lv, ~p"/contacts/#{contact.id}")
end
test "shows errors on invalid submit", %{conn: conn} do
@@ -88,7 +88,7 @@ defmodule MicrowavepropWeb.SubmitLiveTest do
html =
lv
- |> form("#qso-form", qso: %{station1: ""})
+ |> form("#contact-form", contact: %{station1: ""})
|> render_submit()
assert html =~ "can't be blank"
diff --git a/test/mix/tasks/reset_enrichment_test.exs b/test/mix/tasks/reset_enrichment_test.exs
index cb6f8d7c..c12613f6 100644
--- a/test/mix/tasks/reset_enrichment_test.exs
+++ b/test/mix/tasks/reset_enrichment_test.exs
@@ -1,7 +1,7 @@
defmodule Mix.Tasks.ResetEnrichmentTest do
use Microwaveprop.DataCase, async: true
- alias Microwaveprop.Radio.Qso
+ alias Microwaveprop.Radio.Contact
alias Mix.Tasks.ResetEnrichment
defp create_queued_qso do
@@ -20,41 +20,41 @@ defmodule Mix.Tasks.ResetEnrichmentTest do
distance_km: Decimal.new("295")
}
- {:ok, qso} =
- %Qso{}
- |> Qso.changeset(default)
+ {:ok, contact} =
+ %Contact{}
+ |> Contact.changeset(default)
|> Repo.insert()
# Set all queued flags to true (not in changeset cast)
- Qso
- |> where([q], q.id == ^qso.id)
+ Contact
+ |> where([q], q.id == ^contact.id)
|> Repo.update_all(set: [weather_queued: true, hrrr_queued: true, iemre_queued: true, terrain_queued: true])
- Repo.get!(Qso, qso.id)
+ Repo.get!(Contact, contact.id)
end
describe "run/1" do
test "resets weather_queued, hrrr_queued, and iemre_queued to false" do
- qso = create_queued_qso()
+ contact = create_queued_qso()
- assert qso.weather_queued == true
- assert qso.hrrr_queued == true
- assert qso.iemre_queued == true
+ assert contact.weather_queued == true
+ assert contact.hrrr_queued == true
+ assert contact.iemre_queued == true
ResetEnrichment.run([])
- updated = Repo.get!(Qso, qso.id)
+ updated = Repo.get!(Contact, contact.id)
assert updated.weather_queued == false
assert updated.hrrr_queued == false
assert updated.iemre_queued == false
end
test "does not reset terrain_queued" do
- qso = create_queued_qso()
+ contact = create_queued_qso()
ResetEnrichment.run([])
- updated = Repo.get!(Qso, qso.id)
+ updated = Repo.get!(Contact, contact.id)
assert updated.terrain_queued == true
end
|