feat: clickable band filters on PSK Reporter tab, fix sync blocking in refresh
- Fix handle_info(:refresh_spots) to use start_async instead of blocking LiveView process on DB fetches - Add require Logger to fix pre-existing compiler warning - Make band counts clickable: clicking a band filters table to that band's last 100 spots; clicking again clears the filter - Highlight active filter badge, show Clear filter link when filtered - Update subtitle and empty state dynamically based on filter - Filter persists across auto-refresh (60s) - Add 5 test cases for band filtering behavior
This commit is contained in:
parent
b91ad05c78
commit
cdf4c75dd5
2 changed files with 172 additions and 15 deletions
|
|
@ -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
|
|||
<Layouts.app flash={@flash} current_scope={@current_scope} max_width="max-w-none">
|
||||
<.header>
|
||||
PSK Reporter Spots
|
||||
<:subtitle>Last {@limit} spot aggregates from the MQTT firehose</:subtitle>
|
||||
<:subtitle>
|
||||
<%= if @band_filter do %>
|
||||
Last {@limit} <span class="badge badge-sm">{@band_filter}</span> spot aggregates
|
||||
<% else %>
|
||||
Last {@limit} spot aggregates from the MQTT firehose
|
||||
<% end %>
|
||||
</:subtitle>
|
||||
</.header>
|
||||
|
||||
<div class="flex flex-wrap items-baseline gap-x-6 gap-y-1 text-sm mb-4">
|
||||
<span class="font-semibold">
|
||||
Total spots stored: <span class="font-mono">{Format.number(@total_spots)}</span>
|
||||
</span>
|
||||
<span :for={{band, count} <- @band_counts} class="text-base-content/70">
|
||||
<button
|
||||
:for={{band, count} <- @band_counts}
|
||||
phx-click="filter_band"
|
||||
phx-value-band={band}
|
||||
class={[
|
||||
"cursor-pointer transition-colors rounded px-1 -mx-1 py-0.5",
|
||||
if(@band_filter == band,
|
||||
do: "bg-primary/15 text-primary font-semibold",
|
||||
else: "hover:bg-base-300 text-base-content/70"
|
||||
)
|
||||
]}
|
||||
>
|
||||
<span class="badge badge-sm mr-1">{band}</span>
|
||||
<span class="font-mono">{Format.number(count)}</span>
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
:if={@band_filter}
|
||||
phx-click="clear_filter"
|
||||
class="text-xs text-primary hover:underline cursor-pointer"
|
||||
>
|
||||
Clear filter
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
|
|
@ -152,8 +201,12 @@ defmodule MicrowavepropWeb.PskrSpotsLive do
|
|||
</table>
|
||||
</div>
|
||||
|
||||
<p :if={@spots_empty && @total_spots == 0} class="text-sm text-base-content/50 italic mt-4">
|
||||
No PSK Reporter spots received yet. The MQTT listener is running — spots will appear here as they arrive.
|
||||
<p :if={@spots_empty && @total_spots != nil} class="text-sm text-base-content/50 italic mt-4">
|
||||
<%= 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 %>
|
||||
</p>
|
||||
</Layouts.app>
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue