- Add on_the_air boolean to beacons (default true); surfaced as a checkbox on the form, a badge on the index, and in the detail list. - Render a Leaflet map on the beacon show page with a marker at the beacon's lat/lon tooltipped with callsign + frequency. Marker is green when on air, gray when off. - Compute and draw a reception-range estimate as concentric signal- strength rings. New Microwaveprop.Beacons.RangeEstimate solves a link budget (FSPL + O2/H2O absorption from BandConfig) at five RX thresholds (-100 to -145 dBm), then scales by 0.5 + score/100 from the latest Propagation.point_detail at the beacon's grid square, so current HRRR conditions shift the rings in or out. - Re-enable the hourly PropagationGridWorker cron and freshness monitor in dev.exs so dev actually has HRRR-backed scores to feed the new estimator.
124 lines
4 KiB
Elixir
124 lines
4 KiB
Elixir
defmodule MicrowavepropWeb.BeaconLive.Show do
|
||
@moduledoc false
|
||
use MicrowavepropWeb, :live_view
|
||
|
||
alias Microwaveprop.Beacons
|
||
alias Microwaveprop.Beacons.Beacon
|
||
alias Microwaveprop.Beacons.RangeEstimate
|
||
|
||
@impl true
|
||
def render(assigns) do
|
||
~H"""
|
||
<Layouts.app flash={@flash} current_scope={@current_scope}>
|
||
<.header>
|
||
{@beacon.callsign}
|
||
<:subtitle>{@beacon.frequency_mhz} MHz · {@beacon.grid}</:subtitle>
|
||
<:actions>
|
||
<.button navigate={~p"/beacons"}>
|
||
<.icon name="hero-arrow-left" />
|
||
</.button>
|
||
<.button
|
||
:if={admin?(@current_scope)}
|
||
variant="primary"
|
||
navigate={~p"/beacons/#{@beacon}/edit?return_to=show"}
|
||
>
|
||
<.icon name="hero-pencil-square" /> Edit
|
||
</.button>
|
||
</:actions>
|
||
</.header>
|
||
|
||
<div
|
||
id={"beacon-map-" <> @beacon.id}
|
||
phx-hook="BeaconMap"
|
||
phx-update="ignore"
|
||
class="h-96 w-full rounded-lg border border-base-300 mb-2"
|
||
data-lat={@beacon.lat}
|
||
data-lon={@beacon.lon}
|
||
data-label={"#{@beacon.callsign} · #{@beacon.frequency_mhz} MHz"}
|
||
data-on-the-air={to_string(@beacon.on_the_air)}
|
||
data-rings={Jason.encode!(@estimate.rings)}
|
||
>
|
||
</div>
|
||
|
||
<div class="text-xs opacity-70 mb-4 flex flex-wrap items-center gap-3">
|
||
<span>
|
||
Band <strong>{@estimate.band_label}</strong> ·
|
||
EIRP <strong>{@estimate.eirp_dbm} dBm</strong> ·
|
||
Atm loss <strong>{@estimate.atm_per_km} dB/km</strong>
|
||
</span>
|
||
<span>
|
||
Prop score <strong>{@estimate.score}</strong>
|
||
(<strong>{@estimate.score_mult}×</strong> range)
|
||
<%= if @estimate.valid_time do %>
|
||
at {Calendar.strftime(@estimate.valid_time, "%Y-%m-%d %H:%M UTC")}
|
||
<% else %>
|
||
(no HRRR data — using default 50)
|
||
<% end %>
|
||
</span>
|
||
</div>
|
||
|
||
<div class="flex flex-wrap gap-3 text-xs mb-4">
|
||
<%= for ring <- @estimate.rings do %>
|
||
<div class="flex items-center gap-1.5">
|
||
<span
|
||
class="inline-block w-3 h-3 rounded-full border"
|
||
style={"background-color: #{ring.color}; border-color: #{ring.color}"}
|
||
>
|
||
</span>
|
||
<span>
|
||
<strong>{ring.label}</strong>
|
||
({ring.rx_dbm} dBm) → {ring.radius_km} km
|
||
</span>
|
||
</div>
|
||
<% end %>
|
||
</div>
|
||
|
||
<.list>
|
||
<:item title="Frequency (MHz)">{@beacon.frequency_mhz}</:item>
|
||
<:item title="Callsign">{@beacon.callsign}</:item>
|
||
<:item title="Grid">{@beacon.grid}</:item>
|
||
<:item title="Latitude">{@beacon.lat}</:item>
|
||
<:item title="Longitude">{@beacon.lon}</:item>
|
||
<:item title="Power (mW)">{@beacon.power_mw}</:item>
|
||
<:item title="Height above ground (ft)">{@beacon.height_ft}</:item>
|
||
<:item title="On the air">{if @beacon.on_the_air, do: "Yes", else: "No"}</:item>
|
||
</.list>
|
||
</Layouts.app>
|
||
"""
|
||
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(:estimate, RangeEstimate.estimate(beacon))}
|
||
end
|
||
|
||
@impl true
|
||
def handle_info({:updated, %Beacon{id: id} = beacon}, %{assigns: %{beacon: %{id: id}}} = socket) do
|
||
{:noreply,
|
||
socket
|
||
|> assign(:beacon, beacon)
|
||
|> assign(:estimate, RangeEstimate.estimate(beacon))}
|
||
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
|
||
end
|