From d3b96725c8ec83f8653d344bf5e71d7645081205 Mon Sep 17 00:00:00 2001 From: Graham McIntire Date: Sun, 3 May 2026 13:26:57 -0500 Subject: [PATCH] fix(rover-planning): two-line station, drop noise columns, fix verdict badge Path table now shows callsign on top with grid muted below, and the detail-only Min clearance / Diffraction columns are gone. The verdict badge matched lowercase 'clear/blocked/marginal' but the worker stores uppercase 'CLEAR/BLOCKED/FRESNEL_PARTIAL/FRESNEL_MINOR' from TerrainAnalysis, so every cell silently rendered empty. Updated the guard clauses to the actual upstream strings and added Fresnel variants. --- .../live/rover_planning_live/show.ex | 60 +++++++------- .../live/rover_planning_live_test.exs | 82 +++++++++++++++++-- 2 files changed, 105 insertions(+), 37 deletions(-) diff --git a/lib/microwaveprop_web/live/rover_planning_live/show.ex b/lib/microwaveprop_web/live/rover_planning_live/show.ex index 53b8497c..f0913041 100644 --- a/lib/microwaveprop_web/live/rover_planning_live/show.ex +++ b/lib/microwaveprop_web/live/rover_planning_live/show.ex @@ -150,19 +150,24 @@ defmodule MicrowavepropWeb.RoverPlanningLive.Show do ~H|Pending| end - defp verdict_badge("clear") do + defp verdict_badge("CLEAR") do assigns = %{} ~H|Clear| end - defp verdict_badge("blocked") do + defp verdict_badge("BLOCKED") do assigns = %{} ~H|Blocked| end - defp verdict_badge("marginal") do + defp verdict_badge("FRESNEL_PARTIAL") do assigns = %{} - ~H|Marginal| + ~H|Fresnel partial| + end + + defp verdict_badge("FRESNEL_MINOR") do + assigns = %{} + ~H|Fresnel minor| end defp verdict_badge(_), do: "" @@ -209,32 +214,27 @@ defmodule MicrowavepropWeb.RoverPlanningLive.Show do defp format_distance(_), do: "—" - defp format_db(nil), do: "—" - defp format_db(value) when is_number(value), do: "#{Float.round(value * 1.0, 1)} dB" + # Two-line station identity for the path table. Returns + # `{primary, secondary}` — primary is the callsign (or grid when no + # callsign), secondary is a Maidenhead grid (stored or derived from + # lat/lon) when it adds info beyond `primary`. + defp station_lines(%{station: %{} = station}), do: station_lines_for(station) + defp station_lines(_), do: {"—", nil} - defp format_meters(nil), do: "—" - defp format_meters(value) when is_number(value), do: "#{Float.round(value * 1.0, 1)} m" + defp station_lines_for(%{callsign: c} = s) when is_binary(c) and c != "", do: {c, station_grid(s)} - # Grid+callsign-first station label. Stations entered as raw lat/lon - # have no callsign or grid stored, so derive a 6-char grid from the - # coords — that's what the user wants to see, not bare decimals. - defp station_label(%{station: %{} = station}), do: station_label_for(station) - defp station_label(_), do: "—" + defp station_lines_for(%{grid: g}) when is_binary(g) and g != "", do: {g, nil} - defp station_label_for(%{callsign: c, grid: g}) when is_binary(c) and c != "" and is_binary(g) and g != "", - do: "#{c} · #{g}" + defp station_lines_for(%{lat: lat, lon: lon}) when is_number(lat) and is_number(lon), + do: {Maidenhead.from_latlon(lat, lon, 6), nil} - defp station_label_for(%{callsign: c, lat: lat, lon: lon}) - when is_binary(c) and c != "" and is_number(lat) and is_number(lon), - do: "#{c} · #{Maidenhead.from_latlon(lat, lon, 6)}" + defp station_lines_for(_), do: {"—", nil} - defp station_label_for(%{callsign: c}) when is_binary(c) and c != "", do: c - defp station_label_for(%{grid: g}) when is_binary(g) and g != "", do: g + defp station_grid(%{grid: g}) when is_binary(g) and g != "", do: g - defp station_label_for(%{lat: lat, lon: lon}) when is_number(lat) and is_number(lon), - do: Maidenhead.from_latlon(lat, lon, 6) + defp station_grid(%{lat: lat, lon: lon}) when is_number(lat) and is_number(lon), do: Maidenhead.from_latlon(lat, lon, 6) - defp station_label_for(_), do: "—" + defp station_grid(_), do: nil # Endpoint string used for /path?destination=…. Prefers callsign, then # any stored or derived grid (PathLive's LocationResolver re-resolves @@ -440,8 +440,6 @@ defmodule MicrowavepropWeb.RoverPlanningLive.Show do Station Status Distance - Min clearance - Diffraction Verdict @@ -454,17 +452,15 @@ defmodule MicrowavepropWeb.RoverPlanningLive.Show do ]} title={path_url(@mission, path) && "Open in Path Calculator"} > - {station_label(path)} + + <% {primary, secondary} = station_lines(path) %> +
{primary}
+
{secondary}
+ {status_badge(path.status)} {if path.result, do: format_distance(path.result["distance_km"]), else: "—"} - - {if path.result, do: format_meters(path.result["min_clearance_m"]), else: "—"} - - - {if path.result, do: format_db(path.result["diffraction_db"]), else: "—"} - {if path.result, do: verdict_badge(path.result["verdict"]), else: "—"} diff --git a/test/microwaveprop_web/live/rover_planning_live_test.exs b/test/microwaveprop_web/live/rover_planning_live_test.exs index 686d9ac5..52b34248 100644 --- a/test/microwaveprop_web/live/rover_planning_live_test.exs +++ b/test/microwaveprop_web/live/rover_planning_live_test.exs @@ -116,7 +116,7 @@ defmodule MicrowavepropWeb.RoverPlanningLiveTest do refute html =~ ~s(33.189, -96.452) end - test "station label shows callsign + grid together when both are known", %{conn: conn} do + test "station cell renders callsign + grid as separate lines", %{conn: conn} do user = AccountsFixtures.user_fixture() mission = create_mission(user, "Callsign-grid mission") @@ -125,9 +125,81 @@ defmodule MicrowavepropWeb.RoverPlanningLiveTest do {: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" + # Callsign and grid both appear, on separate lines (callsign primary, + # grid as a muted secondary). The legacy one-liner "AA5C · EM13se" + # no longer appears anywhere. + assert html =~ "AA5C" + assert html =~ "EM13se" + refute html =~ "AA5C · EM13se" + end + + test "verdict column renders the badge for completed paths", %{conn: conn} do + user = AccountsFixtures.user_fixture() + mission = create_mission(user, "Verdict mission") + + [path | _] = Repo.all(Path) + + {:ok, _} = + path + |> Ecto.Changeset.change(%{ + status: :complete, + result: %{ + "distance_km" => 12.0, + "verdict" => "CLEAR", + "min_clearance_m" => 50.0, + "diffraction_db" => 0.0 + } + }) + |> Repo.update() + + {:ok, _lv, html} = live(conn, ~p"/rover-planning/#{mission.id}") + assert html =~ "Clear" + end + + test "verdict column maps BLOCKED + FRESNEL_PARTIAL to badges", %{conn: conn} do + user = AccountsFixtures.user_fixture() + + mission = + create_mission(user, "Blocked mission", + locations: [ + %{lat: 32.0, lon: -97.0, status: :good}, + %{lat: 33.0, lon: -96.0, status: :good} + ] + ) + + [p1, p2] = Repo.all(Path) + + {:ok, _} = + p1 + |> Ecto.Changeset.change(%{ + status: :complete, + result: %{"distance_km" => 8.0, "verdict" => "BLOCKED"} + }) + |> Repo.update() + + {:ok, _} = + p2 + |> Ecto.Changeset.change(%{ + status: :complete, + result: %{"distance_km" => 9.0, "verdict" => "FRESNEL_PARTIAL"} + }) + |> Repo.update() + + {:ok, _lv, html} = live(conn, ~p"/rover-planning/#{mission.id}") + assert html =~ "Blocked" + assert html =~ "Fresnel" + end + + test "path table omits min clearance + diffraction columns", %{conn: conn} do + user = AccountsFixtures.user_fixture() + mission = create_mission(user, "Trim columns mission") + + {:ok, _lv, html} = live(conn, ~p"/rover-planning/#{mission.id}") + # Header columns relevant to a planning summary: Station / Status / + # Distance / Verdict. Min clearance + diffraction are detail-level + # values better viewed on the Path Calculator itself. + refute html =~ "Min clearance" + refute html =~ "Diffraction" end test "path rows link to /path with mission params prefilled", %{conn: conn} do @@ -198,7 +270,7 @@ defmodule MicrowavepropWeb.RoverPlanningLiveTest do "distance_km" => 0, "min_clearance_m" => 0, "diffraction_db" => 0, - "verdict" => "clear" + "verdict" => "CLEAR" } }) |> Repo.update()