feat(accounts): auto-prefill home QTH from QRZ on register + backfill on boot
This commit is contained in:
parent
b1fe077863
commit
9796f06ba2
6 changed files with 197 additions and 1 deletions
|
|
@ -48,6 +48,11 @@ config :microwaveprop, MicrowavepropWeb.Endpoint,
|
||||||
# Run Oban jobs inline during tests
|
# Run Oban jobs inline during tests
|
||||||
config :microwaveprop, Oban, testing: :inline
|
config :microwaveprop, Oban, testing: :inline
|
||||||
|
|
||||||
|
# Skip the registration-time QRZ lookup in tests — it needs a Mox stub
|
||||||
|
# that isn't set up across the test suite, and the worker is exercised
|
||||||
|
# directly in its own dedicated test.
|
||||||
|
config :microwaveprop, :enable_home_qth_lookup, false
|
||||||
|
|
||||||
# MetricsLogSuppressionTest hits GET /metrics and asserts no "Sent 200"
|
# MetricsLogSuppressionTest hits GET /metrics and asserts no "Sent 200"
|
||||||
# log line; that path needs PromEx alive to return a 200. We can't
|
# log line; that path needs PromEx alive to return a 200. We can't
|
||||||
# blanket-disable PromEx. Instead, toggle off the Oban-queue poller
|
# blanket-disable PromEx. Instead, toggle off the Oban-queue poller
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,8 @@ defmodule Microwaveprop.Accounts do
|
||||||
alias Microwaveprop.Accounts.UserNotifier
|
alias Microwaveprop.Accounts.UserNotifier
|
||||||
alias Microwaveprop.Accounts.UserToken
|
alias Microwaveprop.Accounts.UserToken
|
||||||
alias Microwaveprop.Repo
|
alias Microwaveprop.Repo
|
||||||
|
|
||||||
## Database getters
|
## Database getters
|
||||||
|
alias Microwaveprop.Workers.UserHomeQthLookupWorker
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Gets a user by email.
|
Gets a user by email.
|
||||||
|
|
@ -132,6 +132,47 @@ defmodule Microwaveprop.Accounts do
|
||||||
%User{}
|
%User{}
|
||||||
|> User.registration_changeset(attrs)
|
|> User.registration_changeset(attrs)
|
||||||
|> Repo.insert()
|
|> Repo.insert()
|
||||||
|
|> maybe_enqueue_home_qth_lookup()
|
||||||
|
end
|
||||||
|
|
||||||
|
defp maybe_enqueue_home_qth_lookup({:ok, %User{id: id, callsign: call} = user}) when is_binary(call) do
|
||||||
|
if home_qth_lookup_enabled?() do
|
||||||
|
worker = UserHomeQthLookupWorker
|
||||||
|
|
||||||
|
if Code.ensure_loaded?(worker) and function_exported?(worker, :new, 1) do
|
||||||
|
_ = %{user_id: id} |> worker.new() |> Oban.insert()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
{:ok, user}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp maybe_enqueue_home_qth_lookup(other), do: other
|
||||||
|
|
||||||
|
defp home_qth_lookup_enabled? do
|
||||||
|
Application.get_env(:microwaveprop, :enable_home_qth_lookup, true)
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Enqueue a `UserHomeQthLookupWorker` job for every user whose
|
||||||
|
`home_grid` is still nil. Safe to call repeatedly — the worker
|
||||||
|
no-ops when the user has set their QTH manually.
|
||||||
|
"""
|
||||||
|
@spec backfill_missing_home_qth() :: :ok
|
||||||
|
def backfill_missing_home_qth do
|
||||||
|
worker = UserHomeQthLookupWorker
|
||||||
|
|
||||||
|
if home_qth_lookup_enabled?() and Code.ensure_loaded?(worker) and function_exported?(worker, :new, 1) do
|
||||||
|
User
|
||||||
|
|> where([u], is_nil(u.home_grid))
|
||||||
|
|> select([u], u.id)
|
||||||
|
|> Repo.all()
|
||||||
|
|> Enum.each(fn id ->
|
||||||
|
_ = %{user_id: id} |> worker.new() |> Oban.insert()
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
:ok
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,13 @@ defmodule Microwaveprop.Application do
|
||||||
# lifetime of the pod.
|
# lifetime of the pod.
|
||||||
{:ok, _pid} = Task.start(fn -> Microwaveprop.Weather.warm_grid_cache_from_latest_profile() end)
|
{:ok, _pid} = Task.start(fn -> Microwaveprop.Weather.warm_grid_cache_from_latest_profile() end)
|
||||||
|
|
||||||
|
# One-shot backfill: enqueue a QRZ-lookup job for every user with a
|
||||||
|
# missing home QTH. Idempotent — the worker no-ops if home_grid is
|
||||||
|
# already set, so re-running on every boot is safe (and useful: any
|
||||||
|
# users registered while the worker module was unavailable get
|
||||||
|
# picked up the next time the pod restarts).
|
||||||
|
{:ok, _pid} = Task.start(fn -> Microwaveprop.Accounts.backfill_missing_home_qth() end)
|
||||||
|
|
||||||
# Load ML model in dev/test only (Nx/Axon not compiled for prod)
|
# Load ML model in dev/test only (Nx/Axon not compiled for prod)
|
||||||
if Application.get_env(:microwaveprop, :load_ml_model, false) do
|
if Application.get_env(:microwaveprop, :load_ml_model, false) do
|
||||||
Microwaveprop.Propagation.load_ml_model()
|
Microwaveprop.Propagation.load_ml_model()
|
||||||
|
|
|
||||||
70
lib/microwaveprop/workers/user_home_qth_lookup_worker.ex
Normal file
70
lib/microwaveprop/workers/user_home_qth_lookup_worker.ex
Normal file
|
|
@ -0,0 +1,70 @@
|
||||||
|
defmodule Microwaveprop.Workers.UserHomeQthLookupWorker do
|
||||||
|
@moduledoc """
|
||||||
|
Looks up a user's callsign via QRZ (with Google Maps fallback) and
|
||||||
|
populates their home QTH fields if they're still empty.
|
||||||
|
|
||||||
|
Enqueued automatically on `Accounts.register_user/1` and reusable for
|
||||||
|
backfilling existing users via:
|
||||||
|
|
||||||
|
Microwaveprop.Repo.all(
|
||||||
|
from u in Microwaveprop.Accounts.User, where: is_nil(u.home_grid)
|
||||||
|
)
|
||||||
|
|> Enum.each(fn user ->
|
||||||
|
%{user_id: user.id}
|
||||||
|
|> Microwaveprop.Workers.UserHomeQthLookupWorker.new()
|
||||||
|
|> Oban.insert()
|
||||||
|
end)
|
||||||
|
"""
|
||||||
|
|
||||||
|
use Oban.Worker, queue: :weather, max_attempts: 3
|
||||||
|
|
||||||
|
alias Microwaveprop.Accounts
|
||||||
|
alias Microwaveprop.Accounts.User
|
||||||
|
alias Microwaveprop.Radio.CallsignClient
|
||||||
|
alias Microwaveprop.Radio.Maidenhead
|
||||||
|
alias Microwaveprop.Repo
|
||||||
|
|
||||||
|
require Logger
|
||||||
|
|
||||||
|
@impl Oban.Worker
|
||||||
|
def perform(%Oban.Job{args: %{"user_id" => user_id}}) do
|
||||||
|
case Repo.get(User, user_id) do
|
||||||
|
nil ->
|
||||||
|
:ok
|
||||||
|
|
||||||
|
%User{home_grid: grid} when is_binary(grid) and grid != "" ->
|
||||||
|
# User already set their home QTH manually; don't overwrite.
|
||||||
|
:ok
|
||||||
|
|
||||||
|
%User{callsign: nil} ->
|
||||||
|
:ok
|
||||||
|
|
||||||
|
%User{callsign: callsign} = user ->
|
||||||
|
do_lookup(user, callsign)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp do_lookup(user, callsign) do
|
||||||
|
case CallsignClient.locate(callsign) do
|
||||||
|
{:ok, %{lat: lat, lon: lon} = result} ->
|
||||||
|
grid = result[:gridsquare] || Maidenhead.from_latlon(lat, lon, 10)
|
||||||
|
attrs = %{"home_grid" => grid, "home_lat" => lat, "home_lon" => lon}
|
||||||
|
|
||||||
|
case Accounts.update_user_home_qth(user, attrs) do
|
||||||
|
{:ok, _user} ->
|
||||||
|
:ok
|
||||||
|
|
||||||
|
{:error, changeset} ->
|
||||||
|
Logger.warning(
|
||||||
|
"UserHomeQthLookupWorker save failed: callsign=#{inspect(callsign)} errors=#{inspect(changeset.errors)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
:ok
|
||||||
|
end
|
||||||
|
|
||||||
|
{:error, reason} ->
|
||||||
|
Logger.info("UserHomeQthLookupWorker lookup failed for #{inspect(callsign)}: #{inspect(reason)}")
|
||||||
|
:ok
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -32,6 +32,17 @@
|
||||||
Used to anchor the rover map (drive radius, distance/bearing labels).
|
Used to anchor the rover map (drive radius, distance/bearing labels).
|
||||||
Enter a Maidenhead grid (e.g. <code>EM13qc</code>) or explicit lat/lon.
|
Enter a Maidenhead grid (e.g. <code>EM13qc</code>) or explicit lat/lon.
|
||||||
</p>
|
</p>
|
||||||
|
<div
|
||||||
|
:if={!@current_scope.user.home_grid}
|
||||||
|
class="alert alert-warning text-xs mt-2"
|
||||||
|
>
|
||||||
|
<.icon name="hero-exclamation-triangle" class="size-4 shrink-0" />
|
||||||
|
<span>
|
||||||
|
Using the default NTMS-area location (EM13).
|
||||||
|
A QRZ lookup for <code>{@current_scope.user.callsign}</code>
|
||||||
|
is queued — check back shortly, or set yours manually below.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<.input
|
<.input
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,62 @@
|
||||||
|
defmodule Microwaveprop.Workers.UserHomeQthLookupWorkerTest do
|
||||||
|
use Microwaveprop.DataCase, async: false
|
||||||
|
use Oban.Testing, repo: Microwaveprop.Repo
|
||||||
|
|
||||||
|
import Microwaveprop.AccountsFixtures
|
||||||
|
|
||||||
|
alias Microwaveprop.Repo
|
||||||
|
alias Microwaveprop.Workers.UserHomeQthLookupWorker
|
||||||
|
|
||||||
|
# The test config disables enqueueing on registration globally so the
|
||||||
|
# rest of the suite doesn't need a Mox stub. We don't toggle that here
|
||||||
|
# — these tests invoke the worker directly via `perform_job/2`, which
|
||||||
|
# is independent of the enqueue flag.
|
||||||
|
|
||||||
|
describe "perform/1" do
|
||||||
|
test "no-ops when the user already has a home_grid set" do
|
||||||
|
user =
|
||||||
|
user_fixture()
|
||||||
|
|> Ecto.Changeset.change(home_grid: "EM13qc", home_lat: 33.1, home_lon: -96.6)
|
||||||
|
|> Repo.update!()
|
||||||
|
|
||||||
|
assert :ok = perform_job(UserHomeQthLookupWorker, %{user_id: user.id})
|
||||||
|
reloaded = Repo.reload(user)
|
||||||
|
assert reloaded.home_grid == "EM13qc"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "no-ops when the user no longer exists" do
|
||||||
|
assert :ok = perform_job(UserHomeQthLookupWorker, %{user_id: Ecto.UUID.generate()})
|
||||||
|
end
|
||||||
|
|
||||||
|
test "populates home QTH from a successful CallsignClient lookup" do
|
||||||
|
user = user_fixture()
|
||||||
|
|
||||||
|
# Stub QRZ.com HTTP layer to return coords for the user's callsign.
|
||||||
|
# The callsign-location pipeline reads either lat/lon or grid from
|
||||||
|
# the QRZ XML; we serve coords directly so the geocoder fallback
|
||||||
|
# isn't exercised.
|
||||||
|
Req.Test.stub(Microwaveprop.Qrz.Client, fn conn ->
|
||||||
|
Plug.Conn.send_resp(conn, 200, """
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<QRZDatabase>
|
||||||
|
<Session><Key>test-session</Key></Session>
|
||||||
|
<Callsign>
|
||||||
|
<call>#{user.callsign}</call>
|
||||||
|
<lat>33.10</lat>
|
||||||
|
<lon>-96.625</lon>
|
||||||
|
<grid>EM13qc</grid>
|
||||||
|
</Callsign>
|
||||||
|
</QRZDatabase>
|
||||||
|
""")
|
||||||
|
end)
|
||||||
|
|
||||||
|
assert :ok = perform_job(UserHomeQthLookupWorker, %{user_id: user.id})
|
||||||
|
|
||||||
|
reloaded = Repo.reload(user)
|
||||||
|
assert is_binary(reloaded.home_grid)
|
||||||
|
assert String.starts_with?(reloaded.home_grid, "EM13qc")
|
||||||
|
assert is_float(reloaded.home_lat)
|
||||||
|
assert is_float(reloaded.home_lon)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
Loading…
Add table
Reference in a new issue