prop/lib/microwaveprop_web/live/rover_planning_live/path_show.ex
Graham McIntire 2851f467a3
feat(rover-planning): path detail redirects to /path with full UI
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.
2026-05-03 14:37:57 -05:00

84 lines
2.9 KiB
Elixir

defmodule MicrowavepropWeb.RoverPlanningLive.PathShow do
@moduledoc """
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
import Ecto.Query
alias Microwaveprop.Radio.Maidenhead
alias Microwaveprop.Repo
alias Microwaveprop.RoverPlanning.Mission
alias Microwaveprop.RoverPlanning.Path
@impl true
def mount(%{"id" => mission_id, "path_id" => path_id}, _session, socket) do
case load_path(mission_id, path_id) do
nil ->
{:ok,
socket
|> put_flash(:error, "Path profile not found.")
|> push_navigate(to: ~p"/rover-planning/#{mission_id}")}
%Path{} = path ->
{:ok, push_navigate(socket, to: live_path_url(path))}
end
end
# 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
query =
URI.encode_query(%{
"source" => src,
"destination" => dst,
"band" => Integer.to_string(band),
"src_height_ft" => Float.to_string((mission.rover_height_ft || 8.0) * 1.0),
"dst_height_ft" => Float.to_string((mission.station_height_ft || 30.0) * 1.0)
})
"/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
~H"""
<Layouts.app flash={@flash} current_scope={@current_scope} max_width="max-w-3xl">
<p class="text-sm text-base-content/60 p-8">Opening Path Calculator…</p>
</Layouts.app>
"""
end
end