From 2851f467a3ef75a091488413a6ed97613f9a7cd1 Mon Sep 17 00:00:00 2001 From: Graham McIntire Date: Sun, 3 May 2026 14:37:57 -0500 Subject: [PATCH] feat(rover-planning): path detail redirects to /path with full UI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The cached path-detail URL was a thinned-down view (geometry + loss-budget summary + score). The user wanted the same rich display /path renders — terrain chart with feet axes + tooltips, conditions, scoring, link/power budgets, forecast — so PathShow now resolves the row's rover/station/band/heights and push_navigates to /path with those params baked in. The cached result map on Path.result is still what the rover-planning show table reads from for fast row rendering; this just bridges the row click to the canonical Path Calculator. --- .../live/rover_planning_live/path_show.ex | 200 ++++-------------- 1 file changed, 36 insertions(+), 164 deletions(-) diff --git a/lib/microwaveprop_web/live/rover_planning_live/path_show.ex b/lib/microwaveprop_web/live/rover_planning_live/path_show.ex index ab9c8339..db56cd93 100644 --- a/lib/microwaveprop_web/live/rover_planning_live/path_show.ex +++ b/lib/microwaveprop_web/live/rover_planning_live/path_show.ex @@ -1,11 +1,12 @@ defmodule MicrowavepropWeb.RoverPlanningLive.PathShow do @moduledoc """ - Renders a single rover-planning Path's cached `result` (terrain - analysis + baseline link budget + propagation score). Avoids the - live recompute that `/path` would do — values come straight from - the row the worker stored. The "Open in Path Calculator" button - hands off to the live `/path` flow when the operator wants to see - the live HRRR-driven loss budget. + Stable permalink for one rover-planning Path. Resolves the saved + rover-location, station, band, and antenna heights into the full + `/path` URL so the operator sees the same terrain chart, conditions + readout, link / power budget, and forecast that the Path Calculator + renders. The cached `result` map on the Path row is still consulted + by the rover-planning show table — this LiveView just bridges the + row click back to the canonical `/path` view. """ use MicrowavepropWeb, :live_view @@ -26,64 +27,16 @@ defmodule MicrowavepropWeb.RoverPlanningLive.PathShow do |> push_navigate(to: ~p"/rover-planning/#{mission_id}")} %Path{} = path -> - {:ok, - assign(socket, - page_title: "Path · #{path.mission.name}", - path: path, - mission: path.mission, - rover: path.rover_location, - station: path.station - )} + {:ok, push_navigate(socket, to: live_path_url(path))} end end - defp load_path(mission_id, path_id) do - with {:ok, mission_uuid} <- Ecto.UUID.cast(mission_id), - {:ok, path_uuid} <- Ecto.UUID.cast(path_id) do - Path - |> where([p], p.id == ^path_uuid and p.mission_id == ^mission_uuid) - |> preload([:rover_location, :station, :mission]) - |> Repo.one() - else - _ -> nil - end - end - - defp band_label(mhz) when is_integer(mhz) and mhz >= 1000 do - ghz = mhz / 1000 - if ghz == trunc(ghz), do: "#{trunc(ghz)} GHz", else: "#{Float.round(ghz, 1)} GHz" - end - - defp band_label(mhz) when is_integer(mhz), do: "#{mhz} MHz" - defp band_label(_), do: "—" - - 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(v) when is_number(v), do: "#{Float.round(v * 1.0, 1)} dB" - - defp format_meters(nil), do: "—" - defp format_meters(v) when is_number(v), do: "#{Float.round(v * 1.0, 1)} m" - - defp verdict_label("CLEAR"), do: {"Clear", "badge badge-success"} - defp verdict_label("BLOCKED"), do: {"Blocked", "badge badge-error"} - defp verdict_label("FRESNEL_PARTIAL"), do: {"Fresnel partial", "badge badge-warning"} - defp verdict_label("FRESNEL_MINOR"), do: {"Fresnel minor", "badge badge-info"} - defp verdict_label(_), do: {"—", "badge"} - - 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: Maidenhead.from_latlon(lat, lon, 8) - - defp station_endpoint(_), do: "" - - # Live /path link for when the user wants the HRRR-driven recompute - # rather than the cached snapshot we render here. - defp live_path_url(%Mission{} = mission, %Path{} = path, rover, station) do + # Builds the `/path` URL with the rover/station/band/heights baked + # in. Source uses a 10-char Maidenhead grid (max precision) so the + # rover spot resolves precisely; destination prefers the station's + # callsign, falling back to grid or coordinates. + defp live_path_url(%Path{} = path) do + %Path{rover_location: rover, station: station, mission: %Mission{} = mission} = path src = Maidenhead.from_latlon(rover.lat, rover.lon, 10) dst = station_endpoint(station) band = path.band_mhz || mission.band_mhz @@ -100,112 +53,31 @@ defmodule MicrowavepropWeb.RoverPlanningLive.PathShow do "/path?" <> query end + 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: Maidenhead.from_latlon(lat, lon, 8) + + defp station_endpoint(_), do: "" + + defp load_path(mission_id, path_id) do + with {:ok, mission_uuid} <- Ecto.UUID.cast(mission_id), + {:ok, path_uuid} <- Ecto.UUID.cast(path_id) do + Path + |> where([p], p.id == ^path_uuid and p.mission_id == ^mission_uuid) + |> preload([:rover_location, :station, :mission]) + |> Repo.one() + else + _ -> nil + end + end + @impl true def render(assigns) do - {verdict_text, verdict_class} = - verdict_label((assigns.path.result || %{})["verdict"]) - - assigns = assign(assigns, verdict_text: verdict_text, verdict_class: verdict_class) - ~H""" - - <.header> - Path profile - <:subtitle> - {Maidenhead.from_latlon(@rover.lat, @rover.lon, 10)} - → {station_endpoint(@station)} - · {band_label(@path.band_mhz || @mission.band_mhz)} - {@verdict_text} - - <:actions> - <.link - navigate={~p"/rover-planning/#{@mission.id}"} - class="btn btn-ghost btn-sm" - > - <.icon name="hero-arrow-left" class="w-4 h-4" /> Back to mission - - <.link - href={live_path_url(@mission, @path, @rover, @station)} - class="btn btn-ghost btn-sm" - > - <.icon name="hero-arrow-top-right-on-square" class="w-4 h-4" /> Recompute live - - - - -
- Path is still {@path.status}. Try again in a moment. -
- -
-
-

Geometry

-
-
-
Distance
-
{format_distance(@path.result["distance_km"])}
-
-
-
Bearing
-
- {format_db(@path.result["bearing_deg"]) |> String.replace(" dB", "°")} -
-
-
-
Min clearance
-
{format_meters(@path.result["min_clearance_m"])}
-
-
-
Max elevation
-
{format_meters(@path.result["max_elevation_m"])}
-
-
-
- -
-

Cached link budget

-
-
-
Free-space loss
-
{format_db(@path.result["free_space_loss_db"])}
-
-
-
Oxygen absorption
-
{format_db(@path.result["oxygen_loss_db"])}
-
-
-
H₂O (baseline 7.5 g/m³)
-
{format_db(@path.result["humidity_loss_db_baseline"])}
-
-
-
Diffraction
-
{format_db(@path.result["diffraction_db"])}
-
-
-
Total baseline
-
{format_db(@path.result["total_baseline_loss_db"])}
-
-
-

- Recompute live for HRRR-driven humidity / rain. -

-
- -
-

Propagation forecast

-

- Cached score (midpoint, mission band): - - {(@path.result["propagation_score"] || "—") |> to_string()} - - / 100 -

-

- Computed at {(@path.computed_at && - Calendar.strftime(@path.computed_at, "%Y-%m-%d %H:%M UTC")) || "—"} -

-
-
+ +

Opening Path Calculator…

""" end