diff --git a/lib/microwaveprop_web/live/rover_planning_live/show.ex b/lib/microwaveprop_web/live/rover_planning_live/show.ex
index 802cf90e..bc643e84 100644
--- a/lib/microwaveprop_web/live/rover_planning_live/show.ex
+++ b/lib/microwaveprop_web/live/rover_planning_live/show.ex
@@ -10,6 +10,7 @@ defmodule MicrowavepropWeb.RoverPlanningLive.Show do
alias Microwaveprop.Radio.Maidenhead
alias Microwaveprop.RoverPlanning
alias Microwaveprop.RoverPlanning.Mission
+ alias Phoenix.LiveView.JS
@impl true
def mount(%{"id" => id}, _session, socket) do
@@ -152,6 +153,9 @@ defmodule MicrowavepropWeb.RoverPlanningLive.Show do
defp format_meters(nil), do: "—"
defp format_meters(value) when is_number(value), do: "#{Float.round(value * 1.0, 1)} m"
+ defp station_label(%{station: %{callsign: c, grid: g}}) when is_binary(c) and c != "" and is_binary(g) and g != "",
+ do: "#{c} · #{g}"
+
defp station_label(%{station: %{callsign: c}}) when is_binary(c) and c != "", do: c
defp station_label(%{station: %{grid: g}}) when is_binary(g) and g != "", do: g
@@ -160,6 +164,41 @@ defmodule MicrowavepropWeb.RoverPlanningLive.Show do
defp station_label(_), do: "—"
+ # Endpoint string used for /path?destination=…. Prefers callsign, then
+ # grid (so PathLive's LocationResolver re-resolves to the same point),
+ # falling back to bare coordinates.
+ defp station_endpoint(%{callsign: c}) when is_binary(c) and c != "", do: c
+ defp station_endpoint(%{grid: g}) when is_binary(g) and g != "", do: g
+
+ defp station_endpoint(%{lat: lat, lon: lon}) when is_number(lat) and is_number(lon), do: "#{lat},#{lon}"
+
+ defp station_endpoint(_), do: ""
+
+ # Builds the /path URL pre-filling source (rover-location, max-precision
+ # grid), destination (station endpoint), band, and rover/station heights
+ # so the user lands on a ready-to-compute Path Calculator screen.
+ defp path_url(_mission, %{rover_location: nil}), do: nil
+ defp path_url(_mission, %{station: nil}), do: nil
+
+ defp path_url(%Mission{} = mission, %{rover_location: %{lat: lat, lon: lon}, station: station})
+ when is_number(lat) and is_number(lon) do
+ src = Maidenhead.from_latlon(lat, lon, 10)
+ dst = station_endpoint(station)
+
+ query =
+ URI.encode_query(%{
+ "source" => src,
+ "destination" => dst,
+ "band" => Integer.to_string(mission.band_mhz),
+ "src_height_ft" => Float.to_string(mission.rover_height_ft * 1.0),
+ "dst_height_ft" => Float.to_string(mission.station_height_ft * 1.0)
+ })
+
+ "/path?" <> query
+ end
+
+ defp path_url(_, _), do: nil
+
@impl true
def render(assigns) do
summary = progress_summary(assigns.paths)
@@ -270,7 +309,14 @@ defmodule MicrowavepropWeb.RoverPlanningLive.Show do
-
+
| {station_label(path)} |
{status_badge(path.status)} |
@@ -297,7 +343,10 @@ defmodule MicrowavepropWeb.RoverPlanningLive.Show do
end
defp rover_location_heading(%{lat: lat, lon: lon}) when is_number(lat) and is_number(lon) do
- grid = Maidenhead.from_latlon(lat, lon, 6)
+ # 10-char grid is the maximum useful Maidenhead precision; anything
+ # finer is below the float lat/lon's resolution. The user wants the
+ # full precision, not a 6-char truncation.
+ grid = Maidenhead.from_latlon(lat, lon, 10)
"#{grid} (#{Float.round(lat, 4)}, #{Float.round(lon, 4)})"
end
diff --git a/test/microwaveprop_web/live/rover_planning_live_test.exs b/test/microwaveprop_web/live/rover_planning_live_test.exs
index fca4121a..c7623346 100644
--- a/test/microwaveprop_web/live/rover_planning_live_test.exs
+++ b/test/microwaveprop_web/live/rover_planning_live_test.exs
@@ -4,6 +4,7 @@ defmodule MicrowavepropWeb.RoverPlanningLiveTest do
import Phoenix.LiveViewTest
alias Microwaveprop.AccountsFixtures
+ alias Microwaveprop.Radio.Maidenhead
alias Microwaveprop.Repo
alias Microwaveprop.Rover
alias Microwaveprop.RoverPlanning
@@ -74,6 +75,50 @@ defmodule MicrowavepropWeb.RoverPlanningLiveTest do
assert html =~ "Path profiles"
end
+ test "group heading uses max-precision Maidenhead grid (10 chars)", %{conn: conn} do
+ user = AccountsFixtures.user_fixture()
+ mission = create_mission(user, "Precision mission")
+
+ {:ok, _lv, html} = live(conn, ~p"/rover-planning/#{mission.id}")
+
+ # Maidenhead.from_latlon(32.0, -97.0, 10) → "EM12VX00AA" (or similar
+ # 10-char grid). The group heading must NOT truncate at 6 chars.
+ grid_10 = Maidenhead.from_latlon(32.0, -97.0, 10)
+ assert String.length(grid_10) == 10
+ assert html =~ grid_10
+ end
+
+ test "station label shows callsign + grid together when both are known", %{conn: conn} do
+ user = AccountsFixtures.user_fixture()
+ mission = create_mission(user, "Callsign-grid mission")
+
+ [path | _] = Path |> Repo.all() |> Repo.preload(:station)
+ {:ok, _} = path.station |> Ecto.Changeset.change(%{callsign: "AA5C", grid: "EM13se"}) |> Repo.update()
+
+ {:ok, _lv, html} = live(conn, ~p"/rover-planning/#{mission.id}")
+
+ # The path table should display "AA5C · EM13se" so the operator can
+ # see both at a glance — not just one or the other and not lat/lon.
+ assert html =~ "AA5C · EM13se"
+ end
+
+ test "path rows link to /path with mission params prefilled", %{conn: conn} do
+ user = AccountsFixtures.user_fixture()
+ mission = create_mission(user, "Click-through mission")
+
+ {:ok, lv, _html} = live(conn, ~p"/rover-planning/#{mission.id}")
+
+ # Each path row carries a phx-click that navigates to /path with
+ # the mission's band + heights and the rover/station endpoints
+ # filled in. Assert the click handler is present on the |
.
+ rendered = render(lv)
+ assert rendered =~ "phx-click"
+ assert rendered =~ "/path?"
+ assert rendered =~ "band=10000"
+ assert rendered =~ "src_height_ft=8.0"
+ assert rendered =~ "dst_height_ft=30.0"
+ end
+
test "groups paths by rover location", %{conn: conn} do
user = AccountsFixtures.user_fixture()