towerops/lib/towerops_web/live/coverage_live/show.ex
Graham McIntire 84d4fccb29 feat: optionally attach a coverage to a device at the chosen site
After picking a site in the coverage form, a new optional dropdown
lets the user attach the coverage to one of the devices at that site.
The dropdown reloads on every site change. If the site has no devices
yet, a hint replaces it.

Schema: nullable device_id FK on coverages (on_delete: nilify_all so
deleting a device leaves the coverage standing). Filtered by both
site_id and organization_id when loading the dropdown options.

Show page lists the attached device by name (or IP) when set.
2026-05-06 14:49:32 -05:00

127 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 =
Coverages.get_coverage!(socket.assigns.organization.id, socket.assigns.coverage.id)
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