towerops/lib/towerops_web/live/coverage_live/show.ex
Graham McIntire ef4a4c99b5 fix(coverage): production show-page crash and stale device on PubSub refresh
- show.html.heex: `@coverage.device_id and ...` raised BadBooleanError
  in production because device_id is a UUID string, not a boolean. Use
  is_nil/1 checks instead.
- show.ex: handle_info/2 was reloading the coverage without preloading
  :device, so the device row vanished from the UI on every status
  broadcast. Preload :device alongside the reload.
- coverage_worker: thread the precomputed tx_height through the pixel
  loop instead of re-deriving `coverage.height_agl_m + ...` per pixel
  with inconsistent nil-safety vs. compute_pixels/6.
2026-05-06 15:19:32 -05:00

129 lines
3.8 KiB
Elixir

defmodule ToweropsWeb.CoverageLive.Show do
@moduledoc false
use ToweropsWeb, :live_view
alias Towerops.Coverages
alias Towerops.Coverages.Antenna
alias Towerops.Coverages.Coverage
@impl true
def mount(%{"id" => id}, _session, socket) do
organization = socket.assigns.current_scope.organization
coverage = organization.id |> Coverages.get_coverage!(id) |> Towerops.Repo.preload(:device)
antenna = Antenna.get(coverage.antenna_slug)
eirp = Coverage.eirp_dbm(coverage, (antenna && antenna.gain_dbi) || 0.0)
if connected?(socket), do: Coverages.subscribe(coverage.id)
{:ok,
socket
|> assign(:page_title, coverage.name)
|> assign(:organization, organization)
|> assign(:coverage, coverage)
|> assign(:antenna, antenna)
|> assign(:eirp_dbm, eirp)}
end
@impl true
def handle_params(_params, _url, socket), do: {:noreply, socket}
@impl true
def handle_event("recompute", _params, socket) do
case Coverages.queue_compute(socket.assigns.coverage) do
{:ok, coverage} ->
{:noreply,
socket
|> assign(:coverage, coverage)
|> put_flash(:info, t("Coverage queued for recompute"))}
{:error, :compute_in_progress} ->
{:noreply, put_flash(socket, :error, t("Coverage is already being computed"))}
{:error, _} ->
{:noreply, put_flash(socket, :error, t("Failed to queue coverage"))}
end
end
@impl true
def handle_event("delete", _params, socket) do
case Coverages.delete_coverage(socket.assigns.coverage) do
{:ok, _} ->
{:noreply,
socket
|> put_flash(:info, t("Coverage deleted"))
|> push_navigate(to: ~p"/coverage")}
{:error, _} ->
{:noreply, put_flash(socket, :error, t("Unable to delete coverage"))}
end
end
@impl true
def handle_info({:coverage_status, _status, _extra}, socket) do
coverage =
socket.assigns.organization.id
|> Coverages.get_coverage!(socket.assigns.coverage.id)
|> Towerops.Repo.preload(:device)
eirp =
Coverage.eirp_dbm(
coverage,
(socket.assigns.antenna && socket.assigns.antenna.gain_dbi) || 0.0
)
{:noreply, socket |> assign(:coverage, coverage) |> assign(:eirp_dbm, eirp)}
end
@doc false
def status_badge_class("draft"), do: "bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-300"
def status_badge_class("queued"), do: "bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300"
def status_badge_class("computing"), do: "bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-300"
def status_badge_class("ready"), do: "bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300"
def status_badge_class("failed"), do: "bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-300"
def status_badge_class(_), do: "bg-gray-100 text-gray-700"
@doc false
def coverage_lat(coverage) do
case Coverage.location(coverage) do
{lat, _lon} -> lat
nil -> nil
end
end
@doc false
def coverage_lon(coverage) do
case Coverage.location(coverage) do
{_lat, lon} -> lon
nil -> nil
end
end
@doc false
def format_ft(nil), do: ""
def format_ft(m) when is_number(m), do: "#{round(m / 0.3048)} ft"
@doc false
def format_mi(nil), do: ""
def format_mi(m) when is_number(m), do: "#{Float.round(m / 1609.344, 2)} mi"
@doc false
def format_ghz(nil), do: ""
def format_ghz(mhz) when is_number(mhz), do: "#{Float.round(mhz / 1000.0, 3)} GHz"
attr :label, :string, required: true
attr :value, :string, required: true
def param_row(assigns) do
~H"""
<div class="flex justify-between gap-3">
<dt class="text-gray-600 dark:text-gray-400">{@label}</dt>
<dd class="text-gray-900 dark:text-gray-100 font-medium text-right">{@value}</dd>
</div>
"""
end
end