diff --git a/lib/microwaveprop/beacon_monitors.ex b/lib/microwaveprop/beacon_monitors.ex
new file mode 100644
index 00000000..1c413368
--- /dev/null
+++ b/lib/microwaveprop/beacon_monitors.ex
@@ -0,0 +1,74 @@
+defmodule Microwaveprop.BeaconMonitors do
+ @moduledoc """
+ The BeaconMonitors context: manages the monitor stations a user
+ has registered. Each monitor has a unique random token the remote
+ program uses to authenticate its reports.
+ """
+
+ import Ecto.Query
+
+ alias Microwaveprop.Accounts.User
+ alias Microwaveprop.BeaconMonitors.BeaconMonitor
+ alias Microwaveprop.Repo
+
+ @token_bytes 32
+
+ @doc """
+ Returns all monitors for the given user, newest first.
+ """
+ def list_monitors_for_user(%User{id: user_id}) do
+ Repo.all(
+ from m in BeaconMonitor,
+ where: m.user_id == ^user_id,
+ order_by: [desc: m.inserted_at]
+ )
+ end
+
+ @doc """
+ Creates a new monitor for the given user with a freshly generated token.
+ """
+ def create_monitor(%User{} = user, attrs) do
+ %BeaconMonitor{user_id: user.id, token: generate_token()}
+ |> BeaconMonitor.changeset(attrs)
+ |> Repo.insert()
+ end
+
+ @doc """
+ Deletes a monitor owned by the given user.
+
+ Returns `{:error, :not_found}` if the monitor does not exist or
+ belongs to another user.
+ """
+ def delete_monitor(%User{id: user_id}, monitor_id) do
+ query =
+ from m in BeaconMonitor,
+ where: m.id == ^monitor_id and m.user_id == ^user_id
+
+ case Repo.one(query) do
+ nil -> {:error, :not_found}
+ monitor -> Repo.delete(monitor)
+ end
+ rescue
+ Ecto.Query.CastError -> {:error, :not_found}
+ end
+
+ @doc """
+ Looks up a monitor by its token. Returns nil if not found.
+ """
+ def get_monitor_by_token(token) when is_binary(token) do
+ Repo.get_by(BeaconMonitor, token: token)
+ end
+
+ @doc """
+ Returns a blank changeset for rendering the new-monitor form.
+ """
+ def change_monitor(attrs \\ %{}) do
+ BeaconMonitor.changeset(%BeaconMonitor{}, attrs)
+ end
+
+ defp generate_token do
+ @token_bytes
+ |> :crypto.strong_rand_bytes()
+ |> Base.url_encode64(padding: false)
+ end
+end
diff --git a/lib/microwaveprop/beacon_monitors/beacon_monitor.ex b/lib/microwaveprop/beacon_monitors/beacon_monitor.ex
new file mode 100644
index 00000000..3ff887bb
--- /dev/null
+++ b/lib/microwaveprop/beacon_monitors/beacon_monitor.ex
@@ -0,0 +1,36 @@
+defmodule Microwaveprop.BeaconMonitors.BeaconMonitor do
+ @moduledoc """
+ A beacon monitor is a remote station running the monitor program
+ that reports beacon reception data. Each monitor is owned by a user
+ and identified by a random token the program uses to authenticate.
+ """
+
+ use Ecto.Schema
+
+ import Ecto.Changeset
+
+ alias Microwaveprop.Accounts.User
+
+ @primary_key {:id, :binary_id, autogenerate: true}
+ @foreign_key_type :binary_id
+ schema "beacon_monitors" do
+ field :name, :string
+ field :token, :string
+ field :last_seen_at, :utc_datetime
+
+ belongs_to :user, User
+
+ timestamps(type: :utc_datetime)
+ end
+
+ @doc """
+ Changeset for user-controlled fields (name only for now).
+ Token and user_id are assigned by the context, not cast from user input.
+ """
+ def changeset(monitor, attrs) do
+ monitor
+ |> cast(attrs, [:name])
+ |> validate_required([:name])
+ |> validate_length(:name, min: 1, max: 100)
+ end
+end
diff --git a/lib/microwaveprop_web/controllers/beacon_monitor_controller.ex b/lib/microwaveprop_web/controllers/beacon_monitor_controller.ex
new file mode 100644
index 00000000..be0690c8
--- /dev/null
+++ b/lib/microwaveprop_web/controllers/beacon_monitor_controller.ex
@@ -0,0 +1,38 @@
+defmodule MicrowavepropWeb.BeaconMonitorController do
+ use MicrowavepropWeb, :controller
+
+ alias Microwaveprop.BeaconMonitors
+
+ def create(conn, %{"beacon_monitor" => params}) do
+ user = conn.assigns.current_scope.user
+
+ case BeaconMonitors.create_monitor(user, params) do
+ {:ok, monitor} ->
+ conn
+ |> put_flash(:info, "Monitor '#{monitor.name}' created. Copy its token below.")
+ |> redirect(to: ~p"/users/settings")
+
+ {:error, changeset} ->
+ message =
+ changeset
+ |> Ecto.Changeset.traverse_errors(fn {msg, _} -> msg end)
+ |> Enum.map_join("; ", fn {k, v} -> "#{k}: #{Enum.join(v, ", ")}" end)
+
+ conn
+ |> put_flash(:error, "Could not create monitor (#{message}).")
+ |> redirect(to: ~p"/users/settings")
+ end
+ end
+
+ def delete(conn, %{"id" => id}) do
+ user = conn.assigns.current_scope.user
+
+ conn =
+ case BeaconMonitors.delete_monitor(user, id) do
+ {:ok, _monitor} -> put_flash(conn, :info, "Monitor deleted.")
+ {:error, :not_found} -> put_flash(conn, :error, "Monitor not found.")
+ end
+
+ redirect(conn, to: ~p"/users/settings")
+ end
+end
diff --git a/lib/microwaveprop_web/controllers/user_settings_controller.ex b/lib/microwaveprop_web/controllers/user_settings_controller.ex
index 629f00fa..887e1fe6 100644
--- a/lib/microwaveprop_web/controllers/user_settings_controller.ex
+++ b/lib/microwaveprop_web/controllers/user_settings_controller.ex
@@ -4,10 +4,12 @@ defmodule MicrowavepropWeb.UserSettingsController do
import MicrowavepropWeb.UserAuth, only: [require_sudo_mode: 2]
alias Microwaveprop.Accounts
+ alias Microwaveprop.BeaconMonitors
alias MicrowavepropWeb.UserAuth
plug :require_sudo_mode
plug :assign_email_and_password_changesets
+ plug :assign_beacon_monitors
def edit(conn, _params) do
render(conn, :edit)
@@ -74,4 +76,12 @@ defmodule MicrowavepropWeb.UserSettingsController do
|> assign(:email_changeset, Accounts.change_user_email(user))
|> assign(:password_changeset, Accounts.change_user_password(user))
end
+
+ defp assign_beacon_monitors(conn, _opts) do
+ user = conn.assigns.current_scope.user
+
+ conn
+ |> assign(:beacon_monitors, BeaconMonitors.list_monitors_for_user(user))
+ |> assign(:beacon_monitor_changeset, BeaconMonitors.change_monitor())
+ end
end
diff --git a/lib/microwaveprop_web/controllers/user_settings_html/edit.html.heex b/lib/microwaveprop_web/controllers/user_settings_html/edit.html.heex
index 6306e80d..ab1ccae6 100644
--- a/lib/microwaveprop_web/controllers/user_settings_html/edit.html.heex
+++ b/lib/microwaveprop_web/controllers/user_settings_html/edit.html.heex
@@ -46,4 +46,80 @@
Save Password
+
+
+
+
+ <.header>
+ Beacon monitors
+ <:subtitle>
+ Register remote monitor stations. Each monitor gets a unique token the
+ monitor program uses to authenticate its reports.
+
+
+
+ <.form
+ :let={f}
+ for={@beacon_monitor_changeset}
+ as={:beacon_monitor}
+ action={~p"/users/beacon-monitors"}
+ id="create_beacon_monitor"
+ class="flex items-end gap-2"
+ >
+
+ <.input
+ field={f[:name]}
+ type="text"
+ label="Monitor name"
+ placeholder="e.g. Shack Pi"
+ required
+ />
+
+ <.button variant="primary" phx-disable-with="Adding...">Add monitor
+
+
+ <%= if @beacon_monitors == [] do %>
+ No monitors registered yet.
+ <% else %>
+
+
+
+
+ | Name |
+ Token |
+ Last seen |
+ |
+
+
+
+ <%= for monitor <- @beacon_monitors do %>
+
+ | {monitor.name} |
+
+ {monitor.token}
+ |
+
+ <%= if monitor.last_seen_at do %>
+ {Calendar.strftime(monitor.last_seen_at, "%Y-%m-%d %H:%M UTC")}
+ <% else %>
+ never
+ <% end %>
+ |
+
+ <.link
+ href={~p"/users/beacon-monitors/#{monitor.id}"}
+ method="delete"
+ class="btn btn-ghost btn-xs text-error"
+ data-confirm={"Delete monitor '#{monitor.name}'? Its token will stop working immediately."}
+ >
+ Delete
+
+ |
+
+ <% end %>
+
+
+
+ <% end %>
+
diff --git a/lib/microwaveprop_web/router.ex b/lib/microwaveprop_web/router.ex
index 7f54d5bd..aa8d8d97 100644
--- a/lib/microwaveprop_web/router.ex
+++ b/lib/microwaveprop_web/router.ex
@@ -86,6 +86,9 @@ defmodule MicrowavepropWeb.Router do
get "/users/settings", UserSettingsController, :edit
put "/users/settings", UserSettingsController, :update
get "/users/settings/confirm-email/:token", UserSettingsController, :confirm_email
+
+ post "/users/beacon-monitors", BeaconMonitorController, :create
+ delete "/users/beacon-monitors/:id", BeaconMonitorController, :delete
end
scope "/", MicrowavepropWeb do
diff --git a/mix.exs b/mix.exs
index 14a19121..9d58b8b3 100644
--- a/mix.exs
+++ b/mix.exs
@@ -58,6 +58,7 @@ defmodule Microwaveprop.MixProject do
{:heroicons,
github: "tailwindlabs/heroicons", tag: "v2.2.0", sparse: "optimized", app: false, compile: false, depth: 1},
{:swoosh, "~> 1.16"},
+ {:gen_smtp, "~> 1.2"},
{:req, "~> 0.5"},
{:telemetry_metrics, "~> 1.0"},
{:telemetry_poller, "~> 1.0"},
diff --git a/mix.lock b/mix.lock
index e48d70b2..8bde97ec 100644
--- a/mix.lock
+++ b/mix.lock
@@ -20,6 +20,7 @@
"file_system": {:hex, :file_system, "1.1.1", "31864f4685b0148f25bd3fbef2b1228457c0c89024ad67f7a81a3ffbc0bbad3a", [:mix], [], "hexpm", "7a15ff97dfe526aeefb090a7a9d3d03aa907e100e262a0f8f7746b78f8f87a5d"},
"finch": {:hex, :finch, "0.21.0", "b1c3b2d48af02d0c66d2a9ebfb5622be5c5ecd62937cf79a88a7f98d48a8290c", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.6.2 or ~> 1.7", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "87dc6e169794cb2570f75841a19da99cfde834249568f2a5b121b809588a4377"},
"fine": {:hex, :fine, "0.1.4", "b19a89c1476c7c57afb5f9314aed5960b5bc95d5277de4cb5ee8e1d1616ce379", [:mix], [], "hexpm", "be3324cc454a42d80951cf6023b9954e9ff27c6daa255483b3e8d608670303f5"},
+ "gen_smtp": {:hex, :gen_smtp, "1.3.0", "62c3d91f0dcf6ce9db71bcb6881d7ad0d1d834c7f38c13fa8e952f4104a8442e", [:rebar3], [{:ranch, ">= 1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "0b73fbf069864ecbce02fe653b16d3f35fd889d0fdd4e14527675565c39d84e6"},
"gettext": {:hex, :gettext, "0.26.2", "5978aa7b21fada6deabf1f6341ddba50bc69c999e812211903b169799208f2a8", [:mix], [{:expo, "~> 0.5.1 or ~> 1.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "aa978504bcf76511efdc22d580ba08e2279caab1066b76bb9aa81c4a1e0a32a5"},
"heroicons": {:git, "https://github.com/tailwindlabs/heroicons.git", "0435d4ca364a608cc75e2f8683d374e55abbae26", [tag: "v2.2.0", sparse: "optimized", depth: 1]},
"hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"},
@@ -46,6 +47,7 @@
"plug_crypto": {:hex, :plug_crypto, "2.1.1", "19bda8184399cb24afa10be734f84a16ea0a2bc65054e23a62bb10f06bc89491", [:mix], [], "hexpm", "6470bce6ffe41c8bd497612ffde1a7e4af67f36a15eea5f921af71cf3e11247c"},
"polaris": {:hex, :polaris, "0.1.0", "dca61b18e3e801ecdae6ac9f0eca5f19792b44a5cb4b8d63db50fc40fc038d22", [:mix], [{:nx, "~> 0.5", [hex: :nx, repo: "hexpm", optional: false]}], "hexpm", "13ef2b166650e533cb24b10e2f3b8ab4f2f449ba4d63156e8c569527f206e2c2"},
"postgrex": {:hex, :postgrex, "0.22.0", "fb027b58b6eab1f6de5396a2abcdaaeb168f9ed4eccbb594e6ac393b02078cbd", [:mix], [{:db_connection, "~> 2.9", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "a68c4261e299597909e03e6f8ff5a13876f5caadaddd0d23af0d0a61afcc5d84"},
+ "ranch": {:hex, :ranch, "2.2.0", "25528f82bc8d7c6152c57666ca99ec716510fe0925cb188172f41ce93117b1b0", [:make, :rebar3], [], "hexpm", "fa0b99a1780c80218a4197a59ea8d3bdae32fbff7e88527d7d8a4787eff4f8e7"},
"req": {:hex, :req, "0.5.17", "0096ddd5b0ed6f576a03dde4b158a0c727215b15d2795e59e0916c6971066ede", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "0b8bc6ffdfebbc07968e59d3ff96d52f2202d0536f10fef4dc11dc02a2a43e39"},
"stream_data": {:hex, :stream_data, "1.3.0", "bde37905530aff386dea1ddd86ecbf00e6642dc074ceffc10b7d4e41dfd6aac9", [:mix], [], "hexpm", "3cc552e286e817dca43c98044c706eec9318083a1480c52ae2688b08e2936e3c"},
"styler": {:hex, :styler, "1.11.0", "35010d970689a23c2bcc8e97bd8bf7d20e3561d60c49be84654df5c37d051a9c", [:mix], [], "hexpm", "70f36165d0cf238a32b7a456fdef6a9c72e77e657d7ac4a0ace33aeba3f2b8c0"},
diff --git a/priv/repo/migrations/20260408154735_create_beacon_monitors.exs b/priv/repo/migrations/20260408154735_create_beacon_monitors.exs
new file mode 100644
index 00000000..40eaef6e
--- /dev/null
+++ b/priv/repo/migrations/20260408154735_create_beacon_monitors.exs
@@ -0,0 +1,18 @@
+defmodule Microwaveprop.Repo.Migrations.CreateBeaconMonitors do
+ use Ecto.Migration
+
+ def change do
+ create table(:beacon_monitors, primary_key: false) do
+ add :id, :binary_id, primary_key: true
+ add :user_id, references(:users, type: :binary_id, on_delete: :delete_all), null: false
+ add :name, :string, null: false
+ add :token, :string, null: false
+ add :last_seen_at, :utc_datetime
+
+ timestamps(type: :utc_datetime)
+ end
+
+ create index(:beacon_monitors, [:user_id])
+ create unique_index(:beacon_monitors, [:token])
+ end
+end
diff --git a/test/microwaveprop/beacon_monitors_test.exs b/test/microwaveprop/beacon_monitors_test.exs
new file mode 100644
index 00000000..7244b968
--- /dev/null
+++ b/test/microwaveprop/beacon_monitors_test.exs
@@ -0,0 +1,99 @@
+defmodule Microwaveprop.BeaconMonitorsTest do
+ use Microwaveprop.DataCase
+
+ import Microwaveprop.AccountsFixtures
+
+ alias Microwaveprop.BeaconMonitors
+ alias Microwaveprop.BeaconMonitors.BeaconMonitor
+
+ describe "create_monitor/2" do
+ test "creates a monitor with a generated token" do
+ user = user_fixture()
+
+ assert {:ok, %BeaconMonitor{} = monitor} =
+ BeaconMonitors.create_monitor(user, %{"name" => "Shack Pi"})
+
+ assert monitor.user_id == user.id
+ assert monitor.name == "Shack Pi"
+ assert is_binary(monitor.token)
+ # strong_rand_bytes(32) |> url_encode64(padding: false) is 43 chars
+ assert String.length(monitor.token) == 43
+ end
+
+ test "generates a unique token per monitor" do
+ user = user_fixture()
+ {:ok, m1} = BeaconMonitors.create_monitor(user, %{"name" => "One"})
+ {:ok, m2} = BeaconMonitors.create_monitor(user, %{"name" => "Two"})
+
+ refute m1.token == m2.token
+ end
+
+ test "requires a name" do
+ user = user_fixture()
+
+ assert {:error, changeset} = BeaconMonitors.create_monitor(user, %{"name" => ""})
+ assert %{name: ["can't be blank"]} = errors_on(changeset)
+ end
+ end
+
+ describe "list_monitors_for_user/1" do
+ test "returns monitors belonging to the user, newest first" do
+ user = user_fixture()
+ other = user_fixture()
+
+ {:ok, first} = BeaconMonitors.create_monitor(user, %{"name" => "First"})
+ Process.sleep(1100)
+ {:ok, second} = BeaconMonitors.create_monitor(user, %{"name" => "Second"})
+ {:ok, _other} = BeaconMonitors.create_monitor(other, %{"name" => "Nope"})
+
+ assert [second_result, first_result] = BeaconMonitors.list_monitors_for_user(user)
+ assert second_result.id == second.id
+ assert first_result.id == first.id
+ end
+
+ test "returns empty list when user has no monitors" do
+ user = user_fixture()
+ assert BeaconMonitors.list_monitors_for_user(user) == []
+ end
+ end
+
+ describe "delete_monitor/2" do
+ test "deletes a monitor owned by the user" do
+ user = user_fixture()
+ {:ok, monitor} = BeaconMonitors.create_monitor(user, %{"name" => "Bye"})
+
+ assert {:ok, %BeaconMonitor{}} = BeaconMonitors.delete_monitor(user, monitor.id)
+ assert BeaconMonitors.list_monitors_for_user(user) == []
+ end
+
+ test "does not delete a monitor owned by another user" do
+ user = user_fixture()
+ other = user_fixture()
+ {:ok, monitor} = BeaconMonitors.create_monitor(other, %{"name" => "Theirs"})
+
+ assert {:error, :not_found} = BeaconMonitors.delete_monitor(user, monitor.id)
+ assert [_] = BeaconMonitors.list_monitors_for_user(other)
+ end
+
+ test "returns :not_found for nonexistent id" do
+ user = user_fixture()
+
+ assert {:error, :not_found} =
+ BeaconMonitors.delete_monitor(user, "11111111-1111-1111-1111-111111111111")
+ end
+ end
+
+ describe "get_monitor_by_token/1" do
+ test "returns the monitor matching the token" do
+ user = user_fixture()
+ {:ok, monitor} = BeaconMonitors.create_monitor(user, %{"name" => "Find Me"})
+
+ found = BeaconMonitors.get_monitor_by_token(monitor.token)
+ assert found.id == monitor.id
+ end
+
+ test "returns nil for unknown token" do
+ refute BeaconMonitors.get_monitor_by_token("not-a-real-token")
+ end
+ end
+end
diff --git a/test/microwaveprop_web/controllers/beacon_monitor_controller_test.exs b/test/microwaveprop_web/controllers/beacon_monitor_controller_test.exs
new file mode 100644
index 00000000..238cac7e
--- /dev/null
+++ b/test/microwaveprop_web/controllers/beacon_monitor_controller_test.exs
@@ -0,0 +1,65 @@
+defmodule MicrowavepropWeb.BeaconMonitorControllerTest do
+ use MicrowavepropWeb.ConnCase, async: true
+
+ import Microwaveprop.AccountsFixtures
+
+ alias Microwaveprop.BeaconMonitors
+
+ setup :register_and_log_in_user
+
+ describe "POST /users/beacon-monitors" do
+ test "creates a monitor for the current user", %{conn: conn, user: user} do
+ conn =
+ post(conn, ~p"/users/beacon-monitors", %{
+ "beacon_monitor" => %{"name" => "Shack Pi"}
+ })
+
+ assert redirected_to(conn) == ~p"/users/settings"
+ assert Phoenix.Flash.get(conn.assigns.flash, :info) =~ "Shack Pi"
+
+ assert [monitor] = BeaconMonitors.list_monitors_for_user(user)
+ assert monitor.name == "Shack Pi"
+ assert is_binary(monitor.token)
+ end
+
+ test "shows an error when name is blank", %{conn: conn, user: user} do
+ conn =
+ post(conn, ~p"/users/beacon-monitors", %{
+ "beacon_monitor" => %{"name" => ""}
+ })
+
+ assert redirected_to(conn) == ~p"/users/settings"
+ assert Phoenix.Flash.get(conn.assigns.flash, :error) =~ "name"
+ assert BeaconMonitors.list_monitors_for_user(user) == []
+ end
+
+ test "redirects unauthenticated users to login" do
+ conn = post(build_conn(), ~p"/users/beacon-monitors", %{"beacon_monitor" => %{"name" => "Nope"}})
+
+ assert redirected_to(conn) == ~p"/users/log-in"
+ end
+ end
+
+ describe "DELETE /users/beacon-monitors/:id" do
+ test "deletes a monitor owned by the current user", %{conn: conn, user: user} do
+ {:ok, monitor} = BeaconMonitors.create_monitor(user, %{"name" => "Bye"})
+
+ conn = delete(conn, ~p"/users/beacon-monitors/#{monitor.id}")
+
+ assert redirected_to(conn) == ~p"/users/settings"
+ assert Phoenix.Flash.get(conn.assigns.flash, :info) =~ "deleted"
+ assert BeaconMonitors.list_monitors_for_user(user) == []
+ end
+
+ test "does not delete a monitor owned by another user", %{conn: conn} do
+ other = user_fixture()
+ {:ok, monitor} = BeaconMonitors.create_monitor(other, %{"name" => "Theirs"})
+
+ conn = delete(conn, ~p"/users/beacon-monitors/#{monitor.id}")
+
+ assert redirected_to(conn) == ~p"/users/settings"
+ assert Phoenix.Flash.get(conn.assigns.flash, :error) =~ "not found"
+ assert [_] = BeaconMonitors.list_monitors_for_user(other)
+ end
+ end
+end