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.
This commit is contained in:
Graham McIntire 2026-05-03 13:26:57 -05:00
parent bd77d1a5b0
commit d3b96725c8
No known key found for this signature in database
GPG key ID: F4ABF488E6029E59
2 changed files with 105 additions and 37 deletions

View file

@ -150,19 +150,24 @@ defmodule MicrowavepropWeb.RoverPlanningLive.Show do
~H|<span class="badge badge-ghost badge-sm">Pending</span>| ~H|<span class="badge badge-ghost badge-sm">Pending</span>|
end end
defp verdict_badge("clear") do defp verdict_badge("CLEAR") do
assigns = %{} assigns = %{}
~H|<span class="badge badge-success badge-xs">Clear</span>| ~H|<span class="badge badge-success badge-xs">Clear</span>|
end end
defp verdict_badge("blocked") do defp verdict_badge("BLOCKED") do
assigns = %{} assigns = %{}
~H|<span class="badge badge-error badge-xs">Blocked</span>| ~H|<span class="badge badge-error badge-xs">Blocked</span>|
end end
defp verdict_badge("marginal") do defp verdict_badge("FRESNEL_PARTIAL") do
assigns = %{} assigns = %{}
~H|<span class="badge badge-warning badge-xs">Marginal</span>| ~H|<span class="badge badge-warning badge-xs">Fresnel partial</span>|
end
defp verdict_badge("FRESNEL_MINOR") do
assigns = %{}
~H|<span class="badge badge-info badge-xs">Fresnel minor</span>|
end end
defp verdict_badge(_), do: "" defp verdict_badge(_), do: ""
@ -209,32 +214,27 @@ defmodule MicrowavepropWeb.RoverPlanningLive.Show do
defp format_distance(_), do: "" defp format_distance(_), do: ""
defp format_db(nil), do: "" # Two-line station identity for the path table. Returns
defp format_db(value) when is_number(value), do: "#{Float.round(value * 1.0, 1)} dB" # `{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 station_lines_for(%{callsign: c} = s) when is_binary(c) and c != "", do: {c, station_grid(s)}
defp format_meters(value) when is_number(value), do: "#{Float.round(value * 1.0, 1)} m"
# Grid+callsign-first station label. Stations entered as raw lat/lon defp station_lines_for(%{grid: g}) when is_binary(g) and g != "", do: {g, nil}
# 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_label_for(%{callsign: c, grid: g}) when is_binary(c) and c != "" and is_binary(g) and g != "", defp station_lines_for(%{lat: lat, lon: lon}) when is_number(lat) and is_number(lon),
do: "#{c} · #{g}" do: {Maidenhead.from_latlon(lat, lon, 6), nil}
defp station_label_for(%{callsign: c, lat: lat, lon: lon}) defp station_lines_for(_), do: {"", nil}
when is_binary(c) and c != "" and is_number(lat) and is_number(lon),
do: "#{c} · #{Maidenhead.from_latlon(lat, lon, 6)}"
defp station_label_for(%{callsign: c}) when is_binary(c) and c != "", do: c defp station_grid(%{grid: g}) when is_binary(g) and g != "", do: g
defp station_label_for(%{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), defp station_grid(%{lat: lat, lon: lon}) when is_number(lat) and is_number(lon), do: Maidenhead.from_latlon(lat, lon, 6)
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 # Endpoint string used for /path?destination=…. Prefers callsign, then
# any stored or derived grid (PathLive's LocationResolver re-resolves # any stored or derived grid (PathLive's LocationResolver re-resolves
@ -440,8 +440,6 @@ defmodule MicrowavepropWeb.RoverPlanningLive.Show do
<th>Station</th> <th>Station</th>
<th>Status</th> <th>Status</th>
<th>Distance</th> <th>Distance</th>
<th>Min clearance</th>
<th>Diffraction</th>
<th>Verdict</th> <th>Verdict</th>
</tr> </tr>
</thead> </thead>
@ -454,17 +452,15 @@ defmodule MicrowavepropWeb.RoverPlanningLive.Show do
]} ]}
title={path_url(@mission, path) && "Open in Path Calculator"} title={path_url(@mission, path) && "Open in Path Calculator"}
> >
<td class="font-mono text-xs">{station_label(path)}</td> <td class="font-mono text-xs">
<% {primary, secondary} = station_lines(path) %>
<div>{primary}</div>
<div :if={secondary} class="opacity-60">{secondary}</div>
</td>
<td>{status_badge(path.status)}</td> <td>{status_badge(path.status)}</td>
<td class="text-xs"> <td class="text-xs">
{if path.result, do: format_distance(path.result["distance_km"]), else: ""} {if path.result, do: format_distance(path.result["distance_km"]), else: ""}
</td> </td>
<td class="text-xs">
{if path.result, do: format_meters(path.result["min_clearance_m"]), else: ""}
</td>
<td class="text-xs">
{if path.result, do: format_db(path.result["diffraction_db"]), else: ""}
</td>
<td> <td>
{if path.result, do: verdict_badge(path.result["verdict"]), else: ""} {if path.result, do: verdict_badge(path.result["verdict"]), else: ""}
</td> </td>

View file

@ -116,7 +116,7 @@ defmodule MicrowavepropWeb.RoverPlanningLiveTest do
refute html =~ ~s(<td class="font-mono text-xs">33.189, -96.452</td>) refute html =~ ~s(<td class="font-mono text-xs">33.189, -96.452</td>)
end 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() user = AccountsFixtures.user_fixture()
mission = create_mission(user, "Callsign-grid mission") 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}") {:ok, _lv, html} = live(conn, ~p"/rover-planning/#{mission.id}")
# The path table should display "AA5C · EM13se" so the operator can # Callsign and grid both appear, on separate lines (callsign primary,
# see both at a glance — not just one or the other and not lat/lon. # grid as a muted secondary). The legacy one-liner "AA5C · EM13se"
assert html =~ "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 end
test "path rows link to /path with mission params prefilled", %{conn: conn} do test "path rows link to /path with mission params prefilled", %{conn: conn} do
@ -198,7 +270,7 @@ defmodule MicrowavepropWeb.RoverPlanningLiveTest do
"distance_km" => 0, "distance_km" => 0,
"min_clearance_m" => 0, "min_clearance_m" => 0,
"diffraction_db" => 0, "diffraction_db" => 0,
"verdict" => "clear" "verdict" => "CLEAR"
} }
}) })
|> Repo.update() |> Repo.update()