feat(profile): show beacon monitors section on own user profile page

Add a Beacon monitors card to /u/:callsign that appears only when the
viewer is the profile owner. Shows registered monitors with truncated
token and last-seen timestamp, with a link to manage monitors in
settings. Includes tests for visibility rules.
This commit is contained in:
Graham McIntire 2026-07-21 18:08:05 -05:00
parent c193f35a0c
commit 739984d3bc
No known key found for this signature in database
GPG key ID: F4ABF488E6029E59
2 changed files with 109 additions and 1 deletions

View file

@ -3,6 +3,7 @@ defmodule MicrowavepropWeb.UserProfileLive do
use MicrowavepropWeb, :live_view
alias Microwaveprop.Accounts
alias Microwaveprop.BeaconMonitors
alias Microwaveprop.Beacons
alias Microwaveprop.Beacons.Beacon
alias Microwaveprop.Format
@ -23,13 +24,22 @@ defmodule MicrowavepropWeb.UserProfileLive do
beacons = Beacons.list_beacons_for_user(user, viewer)
involving = Radio.list_contacts_involving_callsign(user.callsign, viewer)
monitors =
if viewer && viewer.id == user.id do
BeaconMonitors.list_monitors_for_user(user)
else
[]
end
{:ok,
assign(socket,
page_title: user.callsign,
profile: user,
contacts: contacts,
beacons: beacons,
involving: involving
involving: involving,
beacon_monitors: monitors,
is_own_profile: viewer && viewer.id == user.id
)}
end
end
@ -155,6 +165,56 @@ defmodule MicrowavepropWeb.UserProfileLive do
</div>
</div>
<div :if={@is_own_profile} class="card bg-base-200 shadow-sm mb-6">
<div class="card-body">
<h2 class="card-title">Beacon monitors</h2>
<p class="text-sm opacity-70">
Remote monitor stations that report beacon reception data.
</p>
<%= if @beacon_monitors == [] do %>
<div class="flex items-center justify-between">
<p class="text-sm opacity-70">No monitors registered yet.</p>
<.link navigate={~p"/users/settings#beacon-monitors"} class="btn btn-primary btn-sm">
<.icon name="hero-plus" class="size-4" /> Register a monitor
</.link>
</div>
<% else %>
<div class="overflow-x-auto">
<table class="table table-zebra table-sm">
<thead>
<tr>
<th>Name</th>
<th>Token</th>
<th>Last seen</th>
</tr>
</thead>
<tbody>
<tr :for={monitor <- @beacon_monitors}>
<td class="font-semibold">{monitor.name}</td>
<td>
<code class="text-xs select-all">{String.slice(monitor.token, -8, 8)}</code>
</td>
<td class="text-sm opacity-70">
<%= if monitor.last_seen_at do %>
{Calendar.strftime(monitor.last_seen_at, "%Y-%m-%d %H:%M UTC")}
<% else %>
never
<% end %>
</td>
</tr>
</tbody>
</table>
</div>
<div class="flex justify-end">
<.link navigate={~p"/users/settings#beacon-monitors"} class="btn btn-primary btn-sm">
<.icon name="hero-plus" class="size-4" /> Register another monitor
</.link>
</div>
<% end %>
</div>
</div>
<div class="card bg-base-200 shadow-sm">
<div class="card-body">
<h2 class="card-title">Contacts {@profile.callsign} is in</h2>

View file

@ -7,6 +7,7 @@ defmodule MicrowavepropWeb.UserProfileLiveTest do
import Phoenix.LiveViewTest
alias Microwaveprop.Accounts.User
alias Microwaveprop.BeaconMonitors
alias Microwaveprop.Beacons
alias Microwaveprop.Radio.Contact
alias Microwaveprop.Repo
@ -217,5 +218,52 @@ defmodule MicrowavepropWeb.UserProfileLiveTest do
assert html =~ ~r/font-mono[^>]*>\s*W\s*</s
end
test "anonymous visitors do not see beacon monitors section", %{conn: conn} do
user = user_fixture(%{callsign: "W5ISP"})
{:ok, _lv, html} = live(conn, ~p"/u/#{user.callsign}")
refute html =~ "Beacon monitors"
refute html =~ "No monitors registered yet"
end
test "another logged-in user does not see beacon monitors section", %{conn: conn} do
owner = user_fixture(%{callsign: "W5ISP"})
other = user_fixture(%{callsign: "W5OTH"})
conn = log_in_user(conn, other)
{:ok, _lv, html} = live(conn, ~p"/u/#{owner.callsign}")
refute html =~ "Beacon monitors"
refute html =~ "No monitors registered yet"
end
test "owners see empty-state when they have no beacon monitors", %{conn: conn} do
user = user_fixture(%{callsign: "W5ISP"})
conn = log_in_user(conn, user)
{:ok, _lv, html} = live(conn, ~p"/u/#{user.callsign}")
assert html =~ "Beacon monitors"
assert html =~ "No monitors registered yet"
assert html =~ "Register a monitor"
end
test "owners see their registered beacon monitors", %{conn: conn} do
user = user_fixture(%{callsign: "W5ISP"})
{:ok, monitor} = BeaconMonitors.create_monitor(user, %{name: "Shack Pi"})
conn = log_in_user(conn, user)
{:ok, _lv, html} = live(conn, ~p"/u/#{user.callsign}")
assert html =~ "Beacon monitors"
assert html =~ "Shack Pi"
assert html =~ "never"
assert html =~ "Register another monitor"
# Token is shown truncated (last 8 chars)
assert html =~ String.slice(monitor.token, -8, 8)
end
end
end