towerops/lib/towerops_web/live/coverage_live/show.ex
Graham McIntire 5a7cdc7138 refactor: apply custom color palette across entire codebase
- Replace gray->cool-steel, blue/indigo->cerulean, red->sweet-salmon, yellow/amber->wheat
- Dark sidebar: sidebar/footer use cool-steel-800 bg, text lightened for contrast
- ~11,600 color replacements across ~99 files
- Update tests for new color class names
2026-06-23 10:58:48 -05:00

185 lines
5.4 KiB
Elixir

defmodule ToweropsWeb.CoverageLive.Show do
@moduledoc false
use ToweropsWeb, :live_view
alias Towerops.Coverages
alias Towerops.Coverages.Antenna
alias Towerops.Coverages.Coverage
alias ToweropsWeb.CoverageLive.Map, as: CoverageMapView
@impl true
def mount(%{"id" => id}, _session, socket) do
organization = socket.assigns.current_scope.organization
coverage = Coverages.get_coverage_with_device!(organization.id, id)
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)
|> assign(:probe, nil)
|> assign(:probe_units, "ft")
|> assign(:show_params, true)}
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
# Probe (Distance Tool) — clicking the map fires this.
@impl true
def handle_event("probe_point", %{"lat" => lat, "lon" => lon}, socket) do
lat = to_float(lat)
lon = to_float(lon)
# Reuse the org-wide query but only keep this coverage's row.
rows =
socket.assigns.organization.id
|> Coverages.query_point(lat, lon)
|> Enum.filter(&(&1.coverage.id == socket.assigns.coverage.id))
{:noreply, assign(socket, :probe, %{lat: lat, lon: lon, rows: rows})}
end
@impl true
def handle_event("close_probe", _params, socket) do
{:noreply, assign(socket, :probe, nil)}
end
@impl true
def handle_event("set_probe_units", %{"units" => units}, socket) when units in ["ft", "m"] do
{:noreply, assign(socket, :probe_units, units)}
end
@impl true
def handle_event("toggle_params", _params, socket) do
{:noreply, update(socket, :show_params, &(!&1))}
end
@impl true
def handle_info({:coverage_status, _status, _extra}, socket) do
coverage =
Coverages.get_coverage_with_device!(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)
|> push_event("coverage:list-changed", %{
coverages: CoverageMapView.coverages_payload([coverage]),
enabled: [coverage.id]
})}
end
# -- helpers (template) --
defdelegate coverages_payload(coverages), to: CoverageMapView
defdelegate fmt_distance(m, units), to: CoverageMapView
defdelegate fmt_rssi(v), to: CoverageMapView
@doc false
def status_badge_class("draft"),
do: "bg-cool-steel-100 text-cool-steel-700 dark:bg-cool-steel-800 dark:text-cool-steel-300"
def status_badge_class("queued"), do: "bg-cerulean-100 text-cerulean-800 dark:bg-cerulean-900/30 dark:text-cerulean-300"
def status_badge_class("computing"), do: "bg-wheat-100 text-wheat-800 dark:bg-wheat-900/30 dark:text-wheat-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-sweet-salmon-100 text-sweet-salmon-800 dark:bg-sweet-salmon-900/30 dark:text-sweet-salmon-300"
def status_badge_class(_), do: "bg-cool-steel-100 text-cool-steel-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-cool-steel-600 dark:text-cool-steel-400">{@label}</dt>
<dd class="text-cool-steel-900 dark:text-cool-steel-100 font-medium text-right">{@value}</dd>
</div>
"""
end
defp to_float(v) when is_number(v), do: v * 1.0
defp to_float(v) when is_binary(v) do
case Float.parse(v) do
{n, _} -> n
:error -> 0.0
end
end
end