102 lines
3 KiB
Elixir
102 lines
3 KiB
Elixir
defmodule Microwaveprop.Workers.ContactPositionBackfillWorker do
|
|
@moduledoc """
|
|
Hourly safety net that fills in `pos1` / `pos2` / `distance_km` for contacts
|
|
whose grids resolve but whose positions are null.
|
|
|
|
New contacts always have positions because `Radio.create_contact/1` refuses
|
|
to insert without resolvable grids. This worker exists for rows that arrive
|
|
via direct DB writes (bulk imports, manual fixes) which bypass that path.
|
|
"""
|
|
use Oban.Pro.Worker, queue: :admin, max_attempts: 1, unique: [period: 60]
|
|
|
|
import Ecto.Query
|
|
|
|
alias Microwaveprop.Radio
|
|
alias Microwaveprop.Radio.Contact
|
|
alias Microwaveprop.Radio.Maidenhead
|
|
alias Microwaveprop.Repo
|
|
|
|
require Logger
|
|
|
|
@default_limit 500
|
|
|
|
@impl Oban.Pro.Worker
|
|
def process(%Oban.Job{args: args}) do
|
|
limit = Map.get(args, "limit", @default_limit)
|
|
|
|
candidates =
|
|
Contact
|
|
|> where([c], (is_nil(c.pos1) and not is_nil(c.grid1)) or (is_nil(c.pos2) and not is_nil(c.grid2)))
|
|
|> limit(^limit)
|
|
|> Repo.all()
|
|
|
|
count =
|
|
Enum.count(candidates, fn contact ->
|
|
updates = compute_updates(contact)
|
|
|
|
if updates == [] do
|
|
false
|
|
else
|
|
{n, _} =
|
|
Contact
|
|
|> where(id: ^contact.id)
|
|
|> Repo.update_all(set: updates)
|
|
|
|
n == 1
|
|
end
|
|
end)
|
|
|
|
if count > 0 do
|
|
Logger.info("ContactPositionBackfill: populated positions for #{count} contacts")
|
|
end
|
|
|
|
{:ok, %{updated: count}}
|
|
end
|
|
|
|
defp compute_updates(contact) do
|
|
pos1 = maybe_pos(contact.pos1, contact.grid1)
|
|
pos2 = maybe_pos(contact.pos2, contact.grid2)
|
|
|
|
[]
|
|
|> add_pos_change(:pos1, contact.pos1, pos1)
|
|
|> add_pos_change(:pos2, contact.pos2, pos2)
|
|
|> add_distance_change(contact, pos1, pos2)
|
|
|> maybe_touch_updated_at()
|
|
end
|
|
|
|
defp maybe_pos(existing, _grid) when not is_nil(existing), do: existing
|
|
defp maybe_pos(_nil, nil), do: nil
|
|
|
|
defp maybe_pos(_nil, grid) do
|
|
case Maidenhead.to_latlon(grid) do
|
|
{:ok, {lat, lon}} -> %{"lat" => lat, "lon" => lon}
|
|
:error -> nil
|
|
end
|
|
end
|
|
|
|
defp add_pos_change(updates, _field, existing, _new) when not is_nil(existing), do: updates
|
|
defp add_pos_change(updates, _field, _existing, nil), do: updates
|
|
defp add_pos_change(updates, field, _nil, new), do: [{field, new} | updates]
|
|
|
|
# Always overwrite distance_km when both positions resolve — the candidate
|
|
# query only selects contacts with at least one nil pos, so any previously
|
|
# stored distance is tied to a stale grid/pos pair.
|
|
defp add_distance_change(updates, _contact, nil, _pos2), do: updates
|
|
defp add_distance_change(updates, _contact, _pos1, nil), do: updates
|
|
|
|
defp add_distance_change(updates, _contact, pos1, pos2) do
|
|
distance =
|
|
pos1["lat"]
|
|
|> Radio.haversine_km(pos1["lon"], pos2["lat"], pos2["lon"])
|
|
|> round()
|
|
|> Decimal.new()
|
|
|
|
[{:distance_km, distance} | updates]
|
|
end
|
|
|
|
defp maybe_touch_updated_at([]), do: []
|
|
|
|
defp maybe_touch_updated_at(updates) do
|
|
[{:updated_at, DateTime.truncate(DateTime.utc_now(), :second)} | updates]
|
|
end
|
|
end
|