Band {@estimate.band_label} ·
EIRP {@estimate.eirp_dbm} dBm ·
Atm loss {@estimate.atm_per_km} dB/km
Beacon grid score {@estimate.center_score}
<%= if @estimate.valid_time do %>
at {Calendar.strftime(@estimate.valid_time, "%Y-%m-%d %H:%M UTC")}
<% else %>
(no HRRR data — defaulting cells to 50)
<% end %>
· {length(@estimate.cells)}
cells rendered
"""
end
@impl true
def mount(%{"id" => id}, _session, socket) do
if connected?(socket), do: Beacons.subscribe_beacons()
beacon = Beacons.get_beacon!(id)
{:ok,
socket
|> assign(:page_title, "Beacon")
|> assign(:beacon, beacon)
|> assign(:show_coverage, false)
|> assign(:coverage_supported, coverage_supported?(beacon))
# Estimate is lazy — computed on the first toggle-on to avoid
# paying the bbox grid cost on every page load.
|> assign(:estimate, nil)}
end
# Estimated current coverage only makes sense from 5.76 GHz upward,
# where atmospheric attenuation meaningfully shapes the reach. Below
# that the free-space-loss-dominated map is misleading.
defp coverage_supported?(%{frequency_mhz: mhz}) when is_number(mhz), do: mhz >= 5760
defp coverage_supported?(_), do: false
@impl true
def handle_event("toggle_coverage", _params, socket) do
cond do
not socket.assigns.coverage_supported ->
{:noreply, socket}
socket.assigns.show_coverage ->
# Already on, user is turning it off.
{:noreply,
socket
|> assign(:show_coverage, false)
|> push_event("toggle_coverage", %{show: false})}
true ->
# First time turning on (or re-enabling after off) — compute
# the estimate if we don't already have it, then push the
# cell payload to the hook.
estimate = socket.assigns.estimate || RangeEstimate.estimate(socket.assigns.beacon)
{:noreply,
socket
|> assign(:show_coverage, true)
|> assign(:estimate, estimate)
|> push_event("load_coverage", %{
grid_step: estimate.grid_step,
cells: estimate.cells,
show: true
})}
end
end
def handle_event("approve", _params, socket) do
if admin?(socket.assigns.current_scope) do
{:ok, approved} = Beacons.approve_beacon(socket.assigns.beacon)
{:noreply,
socket
|> assign(:beacon, approved)
|> put_flash(:info, "Beacon approved.")}
else
{:noreply, put_flash(socket, :error, "Admins only.")}
end
end
@impl true
def handle_info({:updated, %Beacon{id: id} = beacon}, %{assigns: %{beacon: %{id: id}}} = socket) do
supported = coverage_supported?(beacon)
show? = socket.assigns.show_coverage and supported
# Only recompute the estimate if coverage was already displayed —
# otherwise leave it nil and let the next toggle lazy-load it.
estimate =
if show? and socket.assigns.estimate do
RangeEstimate.estimate(beacon)
else
socket.assigns.estimate
end
socket =
socket
|> assign(:beacon, beacon)
|> assign(:coverage_supported, supported)
|> assign(:show_coverage, show?)
|> assign(:estimate, estimate)
# Re-push fresh cells to the hook if the map is currently visible.
if show? and estimate do
{:noreply,
push_event(socket, "load_coverage", %{
grid_step: estimate.grid_step,
cells: estimate.cells,
show: true
})}
else
{:noreply, socket}
end
end
def handle_info({:deleted, %Beacon{id: id}}, %{assigns: %{beacon: %{id: id}}} = socket) do
{:noreply,
socket
|> put_flash(:error, "This beacon was deleted.")
|> push_navigate(to: ~p"/beacons")}
end
def handle_info({type, %Beacon{}}, socket) when type in [:created, :updated, :deleted] do
{:noreply, socket}
end
defp admin?(%{user: %{is_admin: true}}), do: true
defp admin?(_), do: false
defp bearing_label(nil), do: "Omni"
defp bearing_label("omni"), do: "Omni"
defp bearing_label(value), do: "#{value}°"
defp format_coord(value) when is_float(value), do: :erlang.float_to_binary(value, decimals: 6)
defp format_coord(value), do: to_string(value)
defp path_calc_link(%Beacon{} = beacon) do
base = %{
"destination" => precise_grid(beacon),
"dst_height_ft" => to_string(beacon.height_ft)
}
params = maybe_put(base, "band", BandResolver.resolve_as_string(beacon.frequency_mhz))
"/path?" <> URI.encode_query(params)
end
defp precise_grid(%Beacon{lat: lat, lon: lon}) when is_number(lat) and is_number(lon) do
Maidenhead.from_latlon(lat, lon, 8)
end
defp precise_grid(%Beacon{grid: grid}), do: grid
defp maybe_put(map, _key, nil), do: map
defp maybe_put(map, key, value), do: Map.put(map, key, value)
attr :label, :string, required: true
slot :inner_block, required: true
defp stat_field(assigns) do
~H"""