Registered users can suggest edits to any contact's core fields (callsigns, grids, band, mode, timestamp). Edits enter an admin approval queue with field-by-field diff view. On approve, changes are applied and enrichment re-enqueued if grids/band changed. Users receive email notification on approve or reject. Also updates dependabot.yml for mix ecosystem.
83 lines
2.3 KiB
Elixir
83 lines
2.3 KiB
Elixir
defmodule MicrowavepropWeb.ContactMapLive do
|
|
@moduledoc false
|
|
use MicrowavepropWeb, :live_view
|
|
|
|
import Ecto.Query
|
|
|
|
alias Microwaveprop.Radio.Contact
|
|
alias Microwaveprop.Repo
|
|
|
|
@impl true
|
|
def mount(_params, _session, socket) do
|
|
contacts = load_contacts()
|
|
|
|
{:ok,
|
|
assign(socket,
|
|
page_title: "Contact Map",
|
|
contacts_json: Jason.encode!(contacts),
|
|
contact_count: length(contacts)
|
|
)}
|
|
end
|
|
|
|
defp load_contacts do
|
|
from(c in Contact,
|
|
where: not is_nil(c.pos1) and not is_nil(c.pos2),
|
|
select: %{
|
|
id: c.id,
|
|
pos1: c.pos1,
|
|
pos2: c.pos2,
|
|
band: c.band,
|
|
station1: c.station1,
|
|
station2: c.station2,
|
|
mode: c.mode,
|
|
distance_km: c.distance_km,
|
|
qso_timestamp: c.qso_timestamp
|
|
}
|
|
)
|
|
|> Repo.all()
|
|
|> Enum.map(fn c ->
|
|
lat1 = c.pos1["lat"]
|
|
lon1 = c.pos1["lon"] || c.pos1["lng"]
|
|
lat2 = c.pos2["lat"]
|
|
lon2 = c.pos2["lon"] || c.pos2["lng"]
|
|
band = if c.band, do: Decimal.to_integer(c.band), else: 0
|
|
dist = if c.distance_km, do: Decimal.to_float(c.distance_km)
|
|
ts = if c.qso_timestamp, do: Calendar.strftime(c.qso_timestamp, "%Y-%m-%d %H:%M")
|
|
|
|
if lat1 && lon1 && lat2 && lon2 do
|
|
[lat1, lon1, lat2, lon2, band, c.station1, c.station2, c.mode, dist, ts, c.id]
|
|
end
|
|
end)
|
|
|> Enum.reject(&is_nil/1)
|
|
|> dedup_reciprocals()
|
|
end
|
|
|
|
# Deduplicate reciprocal contacts (A↔B same band/hour = same contact logged by both sides)
|
|
defp dedup_reciprocals(contacts) do
|
|
Enum.uniq_by(contacts, fn [_lat1, _lon1, _lat2, _lon2, band, s1, s2, _mode, _dist, ts, _id] ->
|
|
stations = Enum.sort([s1 || "", s2 || ""])
|
|
hour = if ts, do: String.slice(ts, 0..12), else: ""
|
|
{stations, band, hour}
|
|
end)
|
|
end
|
|
|
|
@impl true
|
|
def render(assigns) do
|
|
~H"""
|
|
<div
|
|
id="contacts-map"
|
|
phx-hook="ContactsMap"
|
|
phx-update="ignore"
|
|
class="fixed inset-0 z-0"
|
|
data-contacts={@contacts_json}
|
|
>
|
|
</div>
|
|
<div class="fixed top-3 left-14 z-[1000] bg-base-100 shadow rounded-box border border-base-300 px-3 py-2 flex flex-col gap-1">
|
|
<div class="font-bold text-sm">Contact Map</div>
|
|
<div id="contact-count" class="text-xs opacity-70">
|
|
{Integer.to_string(@contact_count)} contacts
|
|
</div>
|
|
</div>
|
|
"""
|
|
end
|
|
end
|