+
{if beacon.on_the_air, do: "Yes", else: "No"}
@@ -51,6 +55,44 @@ defmodule MicrowavepropWeb.BeaconLive.Index do
+
+
+ <.header>
+ Pending approval
+ <:subtitle>Submitted beacons waiting for admin review.
+
+
+ <.table
+ id="pending-beacons"
+ rows={@streams.pending}
+ row_click={fn {_id, beacon} -> JS.navigate(~p"/beacons/#{beacon}") end}
+ >
+ <:col :let={{_id, beacon}} label="Frequency (MHz)">{beacon.frequency_mhz}
+ <:col :let={{_id, beacon}} label="Call">{beacon.callsign}
+ <:col :let={{_id, beacon}} label="Grid">{beacon.grid}
+ <:col :let={{_id, beacon}} label="EIRP (mW)">{format_mw(beacon.power_mw)}
+ <:col :let={{_id, beacon}} label="Height AGL (ft)">{beacon.height_ft}
+ <:col :let={{_id, beacon}} label="Submitted">
+ {Calendar.strftime(beacon.inserted_at, "%Y-%m-%d %H:%M UTC")}
+
+ <:action :let={{_id, beacon}}>
+ <.link
+ phx-click={JS.push("approve", value: %{id: beacon.id})}
+ data-confirm="Approve this beacon?"
+ >
+ Approve
+
+
+ <:action :let={{id, beacon}}>
+ <.link
+ phx-click={JS.push("delete", value: %{id: beacon.id}) |> hide("##{id}")}
+ data-confirm="Delete this beacon?"
+ >
+ Delete
+
+
+
+
"""
end
@@ -59,10 +101,19 @@ defmodule MicrowavepropWeb.BeaconLive.Index do
def mount(_params, _session, socket) do
if connected?(socket), do: Beacons.subscribe_beacons()
+ pending =
+ if admin?(socket.assigns.current_scope) do
+ Beacons.list_pending_beacons()
+ else
+ []
+ end
+
{:ok,
socket
|> assign(:page_title, "Beacons")
- |> stream(:beacons, Beacons.list_beacons())}
+ |> assign(:pending, pending)
+ |> stream(:beacons, Beacons.list_beacons())
+ |> stream(:pending, pending)}
end
@impl true
@@ -70,7 +121,26 @@ defmodule MicrowavepropWeb.BeaconLive.Index do
if admin?(socket.assigns.current_scope) do
beacon = Beacons.get_beacon!(id)
{:ok, _} = Beacons.delete_beacon(beacon)
- {:noreply, stream_delete(socket, :beacons, beacon)}
+
+ {:noreply,
+ socket
+ |> stream_delete(:beacons, beacon)
+ |> stream_delete(:pending, beacon)}
+ else
+ {:noreply, put_flash(socket, :error, "Admins only.")}
+ end
+ end
+
+ def handle_event("approve", %{"id" => id}, socket) do
+ if admin?(socket.assigns.current_scope) do
+ beacon = Beacons.get_beacon!(id)
+ {:ok, approved} = Beacons.approve_beacon(beacon)
+
+ {:noreply,
+ socket
+ |> put_flash(:info, "Approved #{approved.callsign}.")
+ |> stream_delete(:pending, beacon)
+ |> stream_insert(:beacons, approved)}
else
{:noreply, put_flash(socket, :error, "Admins only.")}
end
@@ -78,9 +148,45 @@ defmodule MicrowavepropWeb.BeaconLive.Index do
@impl true
def handle_info({type, %Microwaveprop.Beacons.Beacon{}}, socket) when type in [:created, :updated, :deleted] do
- {:noreply, stream(socket, :beacons, Beacons.list_beacons(), reset: true)}
+ pending =
+ if admin?(socket.assigns.current_scope) do
+ Beacons.list_pending_beacons()
+ else
+ []
+ end
+
+ {:noreply,
+ socket
+ |> assign(:pending, pending)
+ |> stream(:beacons, Beacons.list_beacons(), reset: true)
+ |> stream(:pending, pending, reset: true)}
end
defp admin?(%{user: %{is_admin: true}}), do: true
defp admin?(_), do: false
+
+ defp authenticated?(%{user: %{}}), do: true
+ defp authenticated?(_), do: false
+
+ # Format a milliwatt power value without scientific notation.
+ # Keeps up to 3 decimal places, drops trailing zeros.
+ defp format_mw(nil), do: ""
+
+ defp format_mw(mw) when is_float(mw) do
+ if mw == trunc(mw) do
+ Integer.to_string(trunc(mw))
+ else
+ mw |> :erlang.float_to_binary(decimals: 3) |> trim_trailing_zeros()
+ end
+ end
+
+ defp format_mw(mw), do: to_string(mw)
+
+ defp trim_trailing_zeros(str) do
+ if String.contains?(str, ".") do
+ str |> String.trim_trailing("0") |> String.trim_trailing(".")
+ else
+ str
+ end
+ end
end
diff --git a/lib/microwaveprop_web/live/beacon_live/show.ex b/lib/microwaveprop_web/live/beacon_live/show.ex
index 7b097ab1..1ecb6ab3 100644
--- a/lib/microwaveprop_web/live/beacon_live/show.ex
+++ b/lib/microwaveprop_web/live/beacon_live/show.ex
@@ -12,8 +12,20 @@ defmodule MicrowavepropWeb.BeaconLive.Show do
<.header>
{@beacon.callsign}
- <:subtitle>{@beacon.frequency_mhz} MHz · {@beacon.grid}
+ <:subtitle>
+ {@beacon.frequency_mhz} MHz · {@beacon.grid}
+
+ Pending approval
+
+
<:actions>
+ <.button
+ :if={admin?(@current_scope) and not @beacon.approved}
+ variant="primary"
+ phx-click="approve"
+ >
+ <.icon name="hero-check" /> Approve
+
<.button navigate={~p"/beacons"}>
<.icon name="hero-arrow-left" />
@@ -54,7 +66,8 @@ defmodule MicrowavepropWeb.BeaconLive.Show do
<% else %>
(no HRRR data — defaulting cells to 50)
<% end %>
- · {length(@estimate.cells)} cells rendered
+ · {length(@estimate.cells)}
+ cells rendered
@@ -67,8 +80,7 @@ defmodule MicrowavepropWeb.BeaconLive.Show do
>