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>|
end
defp verdict_badge("clear") do
defp verdict_badge("CLEAR") do
assigns = %{}
~H|<span class="badge badge-success badge-xs">Clear</span>|
end
defp verdict_badge("blocked") do
defp verdict_badge("BLOCKED") do
assigns = %{}
~H|<span class="badge badge-error badge-xs">Blocked</span>|
end
defp verdict_badge("marginal") do
defp verdict_badge("FRESNEL_PARTIAL") do
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
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
<th>Station</th>
<th>Status</th>
<th>Distance</th>
<th>Min clearance</th>
<th>Diffraction</th>
<th>Verdict</th>
</tr>
</thead>
@ -454,17 +452,15 @@ defmodule MicrowavepropWeb.RoverPlanningLive.Show do
]}
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 class="text-xs">
{if path.result, do: format_distance(path.result["distance_km"]), else: ""}
</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>
{if path.result, do: verdict_badge(path.result["verdict"]), else: ""}
</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>)
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()