diff --git a/lib/microwaveprop_web/live/rover_planning_live/form.ex b/lib/microwaveprop_web/live/rover_planning_live/form.ex index 899c51d6..ffd28158 100644 --- a/lib/microwaveprop_web/live/rover_planning_live/form.ex +++ b/lib/microwaveprop_web/live/rover_planning_live/form.ex @@ -91,9 +91,12 @@ defmodule MicrowavepropWeb.RoverPlanningLive.Form do stations = Map.get(params, "stations", %{}) next_index = next_station_index(stations) + # Don't include "input" in the new station's params — `used_input?/1` + # treats any field present in params as "used" and would render the + # `validate_required([:input])` error before the user has typed + # anything. Leaving the key out keeps the row pristine until edited. stations = Map.put(stations, Integer.to_string(next_index), %{ - "input" => "", "position" => Integer.to_string(next_index) }) @@ -247,14 +250,20 @@ defmodule MicrowavepropWeb.RoverPlanningLive.Form do <.inputs_for :let={s} field={@form[:stations]}>
-
+ <%!-- + `items-start` keeps the lat/lon row top-aligned even when the + callsign cell grows (e.g. an error message under the input). + The trash button uses `mt-6` to sit beside the input — matches + the label height so it visually aligns with the input field. + --%> +
<.input field={s[:input]} type="text" label="Callsign / grid / lat,lon" placeholder="W5LUA or EM13qc or 32.91, -97.06" /> -
+
<.input field={s[:lat]} type="number" step="any" label="Lat" />
@@ -265,7 +274,7 @@ defmodule MicrowavepropWeb.RoverPlanningLive.Form do type="button" phx-click="remove_station" phx-value-index={s.index} - class="btn btn-ghost btn-sm text-error mb-2" + class="btn btn-ghost btn-sm text-error mt-6" title="Remove station" > <.icon name="hero-trash" class="w-4 h-4" /> diff --git a/lib/microwaveprop_web/live/rover_planning_live/show.ex b/lib/microwaveprop_web/live/rover_planning_live/show.ex index a8d3ed53..d3c5b38d 100644 --- a/lib/microwaveprop_web/live/rover_planning_live/show.ex +++ b/lib/microwaveprop_web/live/rover_planning_live/show.ex @@ -124,12 +124,18 @@ defmodule MicrowavepropWeb.RoverPlanningLive.Show do %{total: total, complete: complete, pending: pending, failed: failed} end - defp format_distance(km) when is_number(km), do: "#{Float.round(km, 1)} km / #{Float.round(km * 0.621371, 1)} mi" + # JSONB round-trips bare zeros as integers (e.g. `0` not `0.0`), and + # `Float.round/2` only accepts floats. Coerce with `* 1.0` everywhere + # we round a value that came out of a `:map` column. + defp format_distance(km) when is_number(km), do: "#{Float.round(km * 1.0, 1)} km / #{Float.round(km * 0.621371, 1)} mi" defp format_distance(_), do: "—" defp format_db(nil), do: "—" - defp format_db(value) when is_number(value), do: "#{Float.round(value, 1)} dB" + defp format_db(value) when is_number(value), do: "#{Float.round(value * 1.0, 1)} dB" + + defp format_meters(nil), do: "—" + defp format_meters(value) when is_number(value), do: "#{Float.round(value * 1.0, 1)} m" defp rover_label(%{rover_location: %{lat: lat, lon: lon}}) when is_number(lat) and is_number(lon) do grid = Maidenhead.from_latlon(lat, lon, 6) @@ -243,9 +249,7 @@ defmodule MicrowavepropWeb.RoverPlanningLive.Show do {if path.result, do: format_distance(path.result["distance_km"]), else: "—"} - {if path.result, - do: "#{Float.round(path.result["min_clearance_m"] || 0.0, 1)} m", - else: "—"} + {if path.result, do: format_meters(path.result["min_clearance_m"]), else: "—"} {if path.result, do: format_db(path.result["diffraction_db"]), else: "—"} diff --git a/test/microwaveprop_web/live/rover_planning_live_test.exs b/test/microwaveprop_web/live/rover_planning_live_test.exs index 994478e9..d775e91e 100644 --- a/test/microwaveprop_web/live/rover_planning_live_test.exs +++ b/test/microwaveprop_web/live/rover_planning_live_test.exs @@ -4,8 +4,10 @@ defmodule MicrowavepropWeb.RoverPlanningLiveTest do import Phoenix.LiveViewTest alias Microwaveprop.AccountsFixtures + alias Microwaveprop.Repo alias Microwaveprop.Rover alias Microwaveprop.RoverPlanning + alias Microwaveprop.RoverPlanning.Path alias Microwaveprop.Terrain.ElevationClient setup do @@ -69,6 +71,34 @@ defmodule MicrowavepropWeb.RoverPlanningLiveTest do assert html =~ "Path profiles" end + test "renders integer path-result values without crashing", %{conn: conn} do + # JSONB round-trips bare zeros as integers (e.g. `0` not `0.0`), so the + # show page MUST tolerate non-float numerics in `result`. Previously + # `Float.round(value, n)` crashed with FunctionClauseError when value + # came back as integer 0. + user = AccountsFixtures.user_fixture() + mission = create_mission(user, "Integer result mission") + + [path | _] = Repo.all(Path) + + {:ok, _} = + path + |> Ecto.Changeset.change(%{ + status: :complete, + result: %{ + "distance_km" => 0, + "min_clearance_m" => 0, + "diffraction_db" => 0, + "verdict" => "clear" + } + }) + |> Repo.update() + + {:ok, _lv, html} = live(conn, ~p"/rover-planning/#{mission.id}") + assert html =~ "Integer result mission" + assert html =~ "Clear" + end + test "redirects when mission missing", %{conn: conn} do missing = Ecto.UUID.generate() @@ -83,6 +113,21 @@ defmodule MicrowavepropWeb.RoverPlanningLiveTest do live(conn, ~p"/rover-planning/new") end + test "clicking 'Add station' does not show 'can't be blank' on the new row", %{conn: conn} do + user = AccountsFixtures.user_fixture() + conn = log_in_user(conn, user) + {:ok, lv, _html} = live(conn, ~p"/rover-planning/new") + + html = + lv + |> element("button[phx-click='add_station']") + |> render_click() + + # The new (untouched) station must not render a validation error until + # the user actually interacts with its input. + refute html =~ "can't be blank" + end + test "typing into the form does not flip the only_known_good checkbox", %{conn: conn} do user = AccountsFixtures.user_fixture() conn = log_in_user(conn, user)