Non-admin users can now submit beacons but they are held in an unapproved state until an admin approves them. Only approved beacons appear in the public list; pending submissions are shown in a separate admin-only section on /beacons with Approve and Delete actions. The show page surfaces a pending badge and an admin-only Approve button when viewing an unapproved beacon. Also formats EIRP (mW) on the index page without scientific notation.
151 lines
4.8 KiB
Elixir
151 lines
4.8 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}
|
|
<span :if={not @beacon.approved} class="badge badge-warning badge-sm ml-2">
|
|
Pending approval
|
|
</span>
|
|
</:subtitle>
|
|
<:actions>
|
|
<.button
|
|
:if={admin?(@current_scope) and not @beacon.approved}
|
|
variant="primary"
|
|
phx-click="approve"
|
|
>
|
|
<.icon name="hero-check" /> Approve
|
|
</.button>
|
|
<.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-grid-step={@estimate.grid_step}
|
|
data-cells={Jason.encode!(@estimate.cells)}
|
|
>
|
|
</div>
|
|
|
|
<div class="text-xs opacity-70 mb-2 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>
|
|
Beacon grid score <strong>{@estimate.center_score}</strong>
|
|
<%= 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 %>
|
|
· <strong>{length(@estimate.cells)}</strong>
|
|
cells rendered
|
|
</span>
|
|
</div>
|
|
|
|
<div class="flex flex-wrap gap-3 text-xs mb-4">
|
|
<%= for tier <- @estimate.tiers do %>
|
|
<div class="flex items-center gap-1.5">
|
|
<span
|
|
class="inline-block w-3 h-3 rounded-sm border"
|
|
style={"background-color: #{tier.color}; border-color: #{tier.color}"}
|
|
>
|
|
</span>
|
|
<span>
|
|
<strong>{tier.label}</strong> (≥ {tier.min_dbm} dBm)
|
|
</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="TX power (EIRP) (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_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
|
|
{: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
|