feat(rover-planning): show callsign + grid (no bare coords) in stationary list

The Stationary-stations summary was rendering whatever the user typed
verbatim ('aa5c 33.1889, -96.4517'). Switch to a pair of helpers that
uppercase the callsign (covering legacy mixed-case rows) and synthesize
a 6-char Maidenhead grid from lat/lon when no grid was stored. Bare
coordinates no longer appear in this list — the grid carries the same
location info in a more amateur-radio-native form.
This commit is contained in:
Graham McIntire 2026-05-03 13:41:53 -05:00
parent 210e941db8
commit 39c4c287a5
No known key found for this signature in database
GPG key ID: F4ABF488E6029E59
2 changed files with 54 additions and 4 deletions

View file

@ -253,6 +253,27 @@ defmodule MicrowavepropWeb.RoverPlanningLive.Show do
defp station_grid(_), do: nil
# Inline label for the "Stationary stations" summary list. Prefers the
# callsign (uppercased — legacy data was stored mixed-case), then the
# grid, then the input string when neither is set. Bare lat/lon
# coordinates are intentionally NOT used as a label here — the grid
# half (via `stationary_grid/1`) carries that location info.
defp stationary_label(%{callsign: c}) when is_binary(c) and c != "", do: String.upcase(c)
defp stationary_label(%{grid: g}) when is_binary(g) and g != "", do: g
defp stationary_label(%{input: i}) when is_binary(i) and i != "" do
if LocationResolver.coordinate_pair?(i), do: nil, else: String.upcase(i)
end
defp stationary_label(_), do: nil
defp stationary_grid(%{grid: g}) when is_binary(g) and g != "", do: g
defp stationary_grid(%{lat: lat, lon: lon}) when is_number(lat) and is_number(lon),
do: Maidenhead.from_latlon(lat, lon, 6)
defp stationary_grid(_), do: nil
# Endpoint string used for /path?destination=…. Prefers callsign, then
# any stored or derived grid (PathLive's LocationResolver re-resolves
# both back to the same point), falling back to bare coordinates only
@ -331,10 +352,11 @@ defmodule MicrowavepropWeb.RoverPlanningLive.Show do
<h3 class="font-semibold mb-2">Stationary stations</h3>
<ul class="text-sm space-y-1">
<li :for={station <- @mission.stations} class="flex flex-wrap gap-2">
<span class="font-mono">{station.callsign || station.grid || station.input}</span>
<span :if={station.grid} class="text-base-content/60">grid {station.grid}</span>
<span class="text-base-content/60 font-mono">
{Float.round(station.lat, 4)}, {Float.round(station.lon, 4)}
<span :if={stationary_label(station)} class="font-mono">
{stationary_label(station)}
</span>
<span :if={stationary_grid(station)} class="text-base-content/60 font-mono">
{stationary_grid(station)}
</span>
</li>
</ul>

View file

@ -231,6 +231,34 @@ defmodule MicrowavepropWeb.RoverPlanningLiveTest do
assert html =~ "Propagation"
end
test "stationary stations list shows uppercase callsign + grid, not bare lat/lon", %{conn: conn} do
import Ecto.Query, only: [from: 2]
user = AccountsFixtures.user_fixture()
mission = create_mission(user, "Stationary label mission")
# Backdate the station to mimic legacy data: callsign persisted in
# lowercase, no stored grid. The display should still render the
# callsign uppercase and synthesize a grid from lat/lon.
mission_id = mission.id
[s1] =
Repo.all(from s in Microwaveprop.RoverPlanning.Station, where: s.mission_id == ^mission_id)
{:ok, _} =
s1
|> Ecto.Changeset.change(%{callsign: "aa5c", grid: nil, lat: 33.1889, lon: -96.4517})
|> Repo.update()
{:ok, _lv, html} = live(conn, ~p"/rover-planning/#{mission.id}")
assert html =~ "AA5C"
# Bare lat/lon must not appear in the stationary-stations list.
refute html =~ "33.1889, -96.4517"
# A grid (derived from lat/lon when none is stored) IS shown.
assert html =~ Maidenhead.from_latlon(33.1889, -96.4517, 6)
end
test "rover-location group heading links to /rover-locations/:id", %{conn: conn} do
user = AccountsFixtures.user_fixture()
mission = create_mission(user, "Linked heading mission")