diff --git a/lib/microwaveprop_web/live/pskr_spots_live.ex b/lib/microwaveprop_web/live/pskr_spots_live.ex index 40d546ee..1cbcf58a 100644 --- a/lib/microwaveprop_web/live/pskr_spots_live.ex +++ b/lib/microwaveprop_web/live/pskr_spots_live.ex @@ -8,6 +8,8 @@ defmodule MicrowavepropWeb.PskrSpotsLive do alias Microwaveprop.Pskr.SpotHourly alias Microwaveprop.Repo + require Logger + @limit 100 @refresh_ms 60_000 @@ -26,6 +28,7 @@ defmodule MicrowavepropWeb.PskrSpotsLive do limit: @limit, total_spots: nil, band_counts: [], + band_filter: nil, spots_empty: spots == [] ) |> stream(:spots, spots, reset: true)} @@ -48,15 +51,35 @@ defmodule MicrowavepropWeb.PskrSpotsLive do @impl true def handle_info(:refresh_spots, socket) do _ = schedule_refresh() + band = socket.assigns.band_filter + spots = fetch_recent_spots(band) + + socket = start_async(socket, :total_spots, fn -> fetch_total_spots() end) + socket = start_async(socket, :band_counts, fn -> fetch_band_counts() end) + + {:noreply, + socket + |> assign(spots_empty: spots == []) + |> stream(:spots, spots, reset: true)} + end + + @impl true + def handle_event("filter_band", %{"band" => band}, socket) do + band_filter = if socket.assigns.band_filter == band, do: nil, else: band + spots = fetch_recent_spots(band_filter) + + {:noreply, + socket + |> assign(band_filter: band_filter, spots_empty: spots == []) + |> stream(:spots, spots, reset: true)} + end + + def handle_event("clear_filter", _params, socket) do spots = fetch_recent_spots() {:noreply, socket - |> assign( - total_spots: fetch_total_spots(), - band_counts: fetch_band_counts(), - spots_empty: spots == [] - ) + |> assign(band_filter: nil, spots_empty: spots == []) |> stream(:spots, spots, reset: true)} end @@ -64,11 +87,13 @@ defmodule MicrowavepropWeb.PskrSpotsLive do Process.send_after(self(), :refresh_spots, @refresh_ms) end - defp fetch_recent_spots do - SpotHourly - |> order_by(desc: :last_spot_at) - |> limit(@limit) - |> Repo.all() + defp fetch_recent_spots(band \\ nil) do + query = from(s in SpotHourly, order_by: [desc: s.last_spot_at], limit: @limit) + + query = + if band, do: from(s in query, where: s.band == ^band), else: query + + Repo.all(query) end defp fetch_total_spots do @@ -100,17 +125,41 @@ defmodule MicrowavepropWeb.PskrSpotsLive do <.header> PSK Reporter Spots - <:subtitle>Last {@limit} spot aggregates from the MQTT firehose + <:subtitle> + <%= if @band_filter do %> + Last {@limit} {@band_filter} spot aggregates + <% else %> + Last {@limit} spot aggregates from the MQTT firehose + <% end %> +
Total spots stored: {Format.number(@total_spots)} - + +
@@ -152,8 +201,12 @@ defmodule MicrowavepropWeb.PskrSpotsLive do
-

- No PSK Reporter spots received yet. The MQTT listener is running — spots will appear here as they arrive. +

+ <%= if @band_filter do %> + No spots found for band {@band_filter}. + <% else %> + No PSK Reporter spots received yet. The MQTT listener is running — spots will appear here as they arrive. + <% end %>

""" diff --git a/test/microwaveprop_web/live/pskr_spots_live_test.exs b/test/microwaveprop_web/live/pskr_spots_live_test.exs index 71458166..9cb854aa 100644 --- a/test/microwaveprop_web/live/pskr_spots_live_test.exs +++ b/test/microwaveprop_web/live/pskr_spots_live_test.exs @@ -187,4 +187,108 @@ defmodule MicrowavepropWeb.PskrSpotsLiveTest do assert html =~ "GRID6" end end + + describe "band filtering" do + test "filtering by band shows only that band's spots", %{conn: conn} do + insert_spot(%{band: "2m", sender_grid: "GRID2M", last_spot_at: now()}) + insert_spot(%{band: "70cm", sender_grid: "GRID70", last_spot_at: now()}) + + {:ok, lv, _html} = live(conn, ~p"/pskreporter") + + # Both bands visible initially + html = render(lv) + assert html =~ "GRID2M" + assert html =~ "GRID70" + + # Click the 2m badge + html = + lv + |> element("button[phx-value-band=\"2m\"]") + |> render_click() + + assert html =~ "GRID2M" + refute html =~ "GRID70" + + # Filter badge should be highlighted + assert html =~ "bg-primary/15" + end + + test "clicking the active band again clears the filter", %{conn: conn} do + insert_spot(%{band: "2m", sender_grid: "GRID2M", last_spot_at: now()}) + insert_spot(%{band: "70cm", sender_grid: "GRID70", last_spot_at: now()}) + + {:ok, lv, _html} = live(conn, ~p"/pskreporter") + + # Filter to 2m + html = + lv + |> element("button[phx-value-band=\"2m\"]") + |> render_click() + + refute html =~ "GRID70" + + # Click 2m again to clear + html = + lv + |> element("button[phx-value-band=\"2m\"]") + |> render_click() + + assert html =~ "GRID2M" + assert html =~ "GRID70" + end + + test "clear filter button appears when filter is active", %{conn: conn} do + insert_spot(%{band: "2m", last_spot_at: now()}) + + {:ok, lv, _html} = live(conn, ~p"/pskreporter") + + # No clear filter button initially + refute render(lv) =~ "Clear filter" + + # Filter by band + html = + lv + |> element("button[phx-value-band=\"2m\"]") + |> render_click() + + assert html =~ "Clear filter" + + # Click clear filter + html = + lv + |> element("button", "Clear filter") + |> render_click() + + refute html =~ "Clear filter" + end + + test "subtitle reflects active filter", %{conn: conn} do + insert_spot(%{band: "70cm", last_spot_at: now()}) + + {:ok, lv, html} = live(conn, ~p"/pskreporter") + + assert html =~ "from the MQTT firehose" + + html = + lv + |> element("button[phx-value-band=\"70cm\"]") + |> render_click() + + assert html =~ "70cm" + refute html =~ "from the MQTT firehose" + end + + test "empty state shows band name when filtered", %{conn: conn} do + insert_spot(%{band: "2m", last_spot_at: now()}) + + {:ok, lv, _html} = live(conn, ~p"/pskreporter") + + html = + lv + |> element("button[phx-value-band=\"70cm\"]") + |> render_click() + + assert html =~ "No spots found for band 70cm" + end + end end