defmodule MicrowavepropWeb.RoverPlanningLive.Show do @moduledoc """ `/rover-planning/:id` mission detail. Renders the planned stations and the matrix of computed (or pending) terrain path profiles from every candidate rover-location. """ use MicrowavepropWeb, :live_view alias Microwaveprop.Accounts.User alias Microwaveprop.Radio.Maidenhead alias Microwaveprop.RoverPlanning alias Microwaveprop.RoverPlanning.Mission @impl true def mount(%{"id" => id}, _session, socket) do case RoverPlanning.get_mission_with_paths(id) do nil -> {:ok, socket |> put_flash(:error, "Mission not found.") |> push_navigate(to: ~p"/rover-planning")} %Mission{} = mission -> if connected?(socket) do Phoenix.PubSub.subscribe(Microwaveprop.PubSub, "rover_planning:#{mission.id}") end {:ok, assign(socket, page_title: "Mission · #{mission.name}", mission: mission, paths: RoverPlanning.list_paths(mission) )} end end @impl true def handle_info({:rover_path_updated, _path_id}, socket) do {:noreply, assign(socket, paths: RoverPlanning.list_paths(socket.assigns.mission))} end @impl true def handle_event("delete", _params, socket) do case current_user(socket) do %User{} = user -> case RoverPlanning.delete_mission(user, socket.assigns.mission.id) do {:ok, _} -> {:noreply, socket |> put_flash(:info, "Mission deleted.") |> push_navigate(to: ~p"/rover-planning")} {:error, :not_found} -> {:noreply, put_flash(socket, :error, "You can only delete your own missions.")} end _ -> {:noreply, put_flash(socket, :error, "Sign in required.")} end end defp current_user(%Phoenix.LiveView.Socket{assigns: assigns}) do case assigns[:current_scope] do %{user: %User{} = user} -> user _ -> nil end end defp can_modify?(%{current_scope: %{user: %User{is_admin: true}}}, _), do: true defp can_modify?(%{current_scope: %{user: %User{id: id}}}, %Mission{user_id: id}) when not is_nil(id), do: true defp can_modify?(_, _), do: false 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), do: "#{mhz} MHz" defp status_badge(:complete) do assigns = %{} ~H|Complete| end defp status_badge(:computing) do assigns = %{} ~H|Computing| end defp status_badge(:failed) do assigns = %{} ~H|Failed| end defp status_badge(_) do assigns = %{} ~H|Pending| end defp verdict_badge("clear") do assigns = %{} ~H|Clear| end defp verdict_badge("blocked") do assigns = %{} ~H|Blocked| end defp verdict_badge("marginal") do assigns = %{} ~H|Marginal| end defp verdict_badge(_), do: "" defp progress_summary(paths) do total = length(paths) complete = Enum.count(paths, &(&1.status == :complete)) pending = Enum.count(paths, &(&1.status in [:pending, :computing])) failed = Enum.count(paths, &(&1.status == :failed)) %{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" defp format_distance(_), do: "—" defp format_db(nil), do: "—" defp format_db(value) when is_number(value), do: "#{Float.round(value, 1)} dB" 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) "#{grid} (#{Float.round(lat, 4)}, #{Float.round(lon, 4)})" end defp rover_label(_), do: "—" defp station_label(%{station: %{callsign: c}}) when is_binary(c) and c != "", do: c defp station_label(%{station: %{grid: g}}) when is_binary(g) and g != "", do: g defp station_label(%{station: %{lat: lat, lon: lon}}) when is_number(lat) and is_number(lon), do: "#{Float.round(lat, 3)}, #{Float.round(lon, 3)}" defp station_label(_), do: "—" @impl true def render(assigns) do summary = progress_summary(assigns.paths) assigns = assign(assigns, :summary, summary) ~H""" <.header> {@mission.name} <:subtitle> {band_label(@mission.band_mhz)} · {length(@mission.stations)} station(s) · rover {@mission.rover_height_ft}ft / station {@mission.station_height_ft}ft <:actions> <.link navigate={~p"/rover-planning"} class="btn btn-ghost btn-sm"> <.icon name="hero-arrow-left" class="w-4 h-4" /> All missions <.link :if={can_modify?(assigns, @mission)} navigate={~p"/rover-planning/#{@mission.id}/edit"} class="btn btn-ghost btn-sm" > <.icon name="hero-pencil-square" class="w-4 h-4" /> Edit

Stationary stations

{@mission.notes}

Scope: {if @mission.only_known_good, do: "Known good locations only (status = good)", else: "All rover locations"} · Owner: {(@mission.user && @mission.user.callsign) || "—"}

Path profiles

{@summary.complete}/{@summary.total} complete 0}>· {@summary.pending} pending 0} class="text-error"> · {@summary.failed} failed
No paths yet — they'll appear here as the workers run.
Rover location Station Status Distance Min clearance Diffraction Verdict
{rover_label(path)} {station_label(path)} {status_badge(path.status)} {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_db(path.result["diffraction_db"]), else: "—"} {if path.result, do: verdict_badge(path.result["verdict"]), else: "—"}
""" end end