diff --git a/lib/towerops/beacons.ex b/lib/towerops/beacons.ex
deleted file mode 100644
index e9f8e766..00000000
--- a/lib/towerops/beacons.ex
+++ /dev/null
@@ -1,145 +0,0 @@
-defmodule Towerops.Beacons do
- @moduledoc """
- The Beacons context for managing beacon monitors.
-
- Beacon monitors continuously check network endpoints (HTTP, TCP, DNS, SSL, ICMP)
- from distributed locations and track their availability status over time.
- """
-
- alias Towerops.Beacons.BeaconMonitor
- alias Towerops.Beacons.BeaconMonitorQuery
- alias Towerops.Repo
-
- # ── Queries ────────────────────────────────────────
-
- @doc """
- Lists all beacon monitors for an organization, ordered by name.
- """
- @spec list_organization_beacons(Ecto.UUID.t()) :: [BeaconMonitor.t()]
- def list_organization_beacons(organization_id) do
- BeaconMonitorQuery.base()
- |> BeaconMonitorQuery.for_organization(organization_id)
- |> BeaconMonitorQuery.order_by_name()
- |> Repo.all()
- end
-
- @doc """
- Lists beacon monitors for an organization that were created by a specific user.
- """
- @spec list_user_beacons(Ecto.UUID.t(), Ecto.UUID.t()) :: [BeaconMonitor.t()]
- def list_user_beacons(organization_id, user_id) do
- BeaconMonitorQuery.base()
- |> BeaconMonitorQuery.for_organization(organization_id)
- |> BeaconMonitorQuery.for_user(user_id)
- |> BeaconMonitorQuery.order_by_name()
- |> Repo.all()
- end
-
- @doc """
- Lists all beacon monitors across all organizations, with associations preloaded.
- """
- @spec list_all_beacons() :: [BeaconMonitor.t()]
- def list_all_beacons do
- BeaconMonitorQuery.base()
- |> BeaconMonitorQuery.preload_all()
- |> BeaconMonitorQuery.order_by_name()
- |> Repo.all()
- end
-
- @doc """
- Gets a single beacon monitor by ID. Returns `nil` if not found.
- """
- @spec get_beacon(Ecto.UUID.t()) :: BeaconMonitor.t() | nil
- def get_beacon(id), do: Repo.get(BeaconMonitor, id)
-
- @doc """
- Gets a single beacon monitor by ID, raising `Ecto.NoResultsError` if not found.
- """
- @spec get_beacon!(Ecto.UUID.t()) :: BeaconMonitor.t()
- def get_beacon!(id), do: Repo.get!(BeaconMonitor, id)
-
- @doc """
- Gets a beacon monitor by ID scoped to an organization.
- Returns `nil` if the beacon doesn't exist or belongs to a different organization.
- """
- @spec get_beacon_for_org(Ecto.UUID.t(), Ecto.UUID.t()) :: BeaconMonitor.t() | nil
- def get_beacon_for_org(id, organization_id) do
- Repo.get_by(BeaconMonitor, id: id, organization_id: organization_id)
- end
-
- @doc """
- Gets a beacon monitor by ID scoped to a user.
- Returns `nil` if the beacon doesn't exist or belongs to a different user.
- """
- @spec get_beacon_for_user(Ecto.UUID.t(), Ecto.UUID.t()) :: BeaconMonitor.t() | nil
- def get_beacon_for_user(id, user_id) do
- Repo.get_by(BeaconMonitor, id: id, user_id: user_id)
- end
-
- # ── CRUD ──────────────────────────────────────────
-
- @doc """
- Creates a new beacon monitor with the given attributes.
- """
- @spec create_beacon(map()) :: {:ok, BeaconMonitor.t()} | {:error, Ecto.Changeset.t()}
- def create_beacon(attrs) do
- %BeaconMonitor{}
- |> BeaconMonitor.changeset(attrs)
- |> Repo.insert()
- end
-
- @doc """
- Updates an existing beacon monitor with the given attributes.
- """
- @spec update_beacon(BeaconMonitor.t(), map()) ::
- {:ok, BeaconMonitor.t()} | {:error, Ecto.Changeset.t()}
- def update_beacon(%BeaconMonitor{} = beacon, attrs) do
- beacon
- |> BeaconMonitor.changeset(attrs)
- |> Repo.update()
- end
-
- @doc """
- Deletes a beacon monitor.
- """
- @spec delete_beacon(BeaconMonitor.t()) :: {:ok, BeaconMonitor.t()} | {:error, Ecto.Changeset.t()}
- def delete_beacon(%BeaconMonitor{} = beacon), do: Repo.delete(beacon)
-
- @doc """
- Toggles the enabled state of a beacon monitor.
- """
- @spec toggle_beacon(BeaconMonitor.t()) ::
- {:ok, BeaconMonitor.t()} | {:error, Ecto.Changeset.t()}
- def toggle_beacon(%BeaconMonitor{enabled: false} = beacon), do: update_beacon(beacon, %{enabled: true})
-
- def toggle_beacon(%BeaconMonitor{enabled: true} = beacon), do: update_beacon(beacon, %{enabled: false})
-
- # ── Monitoring Updates ───────────────────────────
-
- @doc """
- Records the result of a beacon check execution.
-
- Updates the beacon's last status, response time, check count, and failure count.
- """
- @spec record_check_result(BeaconMonitor.t(), String.t(), number()) ::
- {:ok, BeaconMonitor.t()} | {:error, Ecto.Changeset.t()}
- def record_check_result(%BeaconMonitor{} = beacon, status, response_time_ms) do
- last_response_time_ms = response_time_ms / 1
- checks_count = beacon.checks_count + 1
- failures_count = increment_failures(status, beacon.failures_count)
-
- update_beacon(beacon, %{
- last_status: status,
- last_response_time_ms: last_response_time_ms,
- last_checked_at: DateTime.utc_now(),
- checks_count: checks_count,
- failures_count: failures_count
- })
- end
-
- # A "up" status does not increment the failure count
- defp increment_failures("up", current), do: current
-
- # Any other status increments the failure count
- defp increment_failures(_status, current), do: current + 1
-end
diff --git a/lib/towerops/beacons/beacon_monitor.ex b/lib/towerops/beacons/beacon_monitor.ex
deleted file mode 100644
index f92a2829..00000000
--- a/lib/towerops/beacons/beacon_monitor.ex
+++ /dev/null
@@ -1,108 +0,0 @@
-defmodule Towerops.Beacons.BeaconMonitor do
- @moduledoc """
- Represents a beacon monitor configuration.
-
- A beacon monitor continuously checks a network endpoint (HTTP, TCP, DNS, SSL, ICMP)
- from distributed locations and records its availability status.
- """
-
- use Towerops.Schema
-
- alias Ecto.Association.NotLoaded
- alias Towerops.Accounts.User
- alias Towerops.Organizations.Organization
-
- @type t :: %__MODULE__{
- id: Ecto.UUID.t(),
- name: String.t(),
- organization_id: Ecto.UUID.t(),
- user_id: Ecto.UUID.t(),
- check_type: String.t(),
- target_url: String.t(),
- target_port: integer() | nil,
- interval_seconds: integer(),
- timeout_ms: integer(),
- config: map(),
- enabled: boolean(),
- last_status: String.t() | nil,
- last_response_time_ms: float() | nil,
- last_checked_at: DateTime.t() | nil,
- checks_count: integer(),
- failures_count: integer(),
- inserted_at: DateTime.t(),
- updated_at: DateTime.t(),
- organization: Organization.t() | NotLoaded.t(),
- user: User.t() | NotLoaded.t()
- }
-
- @check_types ~w(http tcp dns ssl icmp)
-
- schema "beacon_monitors" do
- field :name, :string
- field :check_type, :string, default: "http"
- field :target_url, :string
- field :target_port, :integer
- field :interval_seconds, :integer, default: 60
- field :timeout_ms, :integer, default: 5000
- field :config, :map, default: %{}
- field :enabled, :boolean, default: true
- field :last_status, :string
- field :last_response_time_ms, :float
- field :last_checked_at, :utc_datetime_usec
- field :checks_count, :integer, default: 0
- field :failures_count, :integer, default: 0
-
- belongs_to :organization, Organization
- belongs_to :user, User
-
- timestamps()
- end
-
- @doc """
- Builds a changeset for creating or updating a beacon monitor.
- """
- @spec changeset(t(), map()) :: Ecto.Changeset.t()
- def changeset(beacon, attrs) do
- beacon
- |> cast(attrs, [
- :name,
- :check_type,
- :target_url,
- :target_port,
- :interval_seconds,
- :timeout_ms,
- :config,
- :enabled,
- :organization_id,
- :user_id,
- :last_status,
- :last_response_time_ms,
- :last_checked_at,
- :checks_count,
- :failures_count
- ])
- |> validate_required([:name, :check_type, :target_url, :interval_seconds, :timeout_ms, :organization_id, :user_id])
- |> validate_inclusion(:check_type, @check_types)
- |> validate_number(:interval_seconds, greater_than: 0, less_than: 86_401)
- |> validate_number(:timeout_ms, greater_than: 0, less_than: 60_001)
- |> validate_format(:target_url, ~r/^(https?:\/\/|tcp:\/\/).+|^[\w.-]+$/)
- |> validate_port_or_default()
- end
-
- # Port is optional for HTTP, DNS, and SSL checks
- defp validate_port_or_default(cs) do
- check_type = Ecto.Changeset.get_field(cs, :check_type)
-
- if check_type in ~w(http dns ssl) do
- cs
- else
- target_port = Ecto.Changeset.get_field(cs, :target_port)
-
- if is_integer(target_port) and target_port > 0 and target_port < 65_536 do
- cs
- else
- add_error(cs, :target_port, "must be between 1 and 65535")
- end
- end
- end
-end
diff --git a/lib/towerops/beacons/beacon_monitor_query.ex b/lib/towerops/beacons/beacon_monitor_query.ex
deleted file mode 100644
index b1a7f3bf..00000000
--- a/lib/towerops/beacons/beacon_monitor_query.ex
+++ /dev/null
@@ -1,54 +0,0 @@
-defmodule Towerops.Beacons.BeaconMonitorQuery do
- @moduledoc """
- Query builder for `BeaconMonitor` records.
-
- Provides composable filter and ordering functions that can be pipelined
- to build complex queries.
- """
-
- import Ecto.Query
-
- alias Towerops.Beacons.BeaconMonitor
-
- @doc """
- Returns a base query on the `beacon_monitors` table.
- """
- @spec base() :: Ecto.Query.t()
- def base, do: from(b in BeaconMonitor, as: :beacon_monitor)
-
- @doc """
- Filters to beacons belonging to the given organization.
- """
- @spec for_organization(Ecto.Query.t(), Ecto.UUID.t()) :: Ecto.Query.t()
- def for_organization(query, organization_id), do: from(q in query, where: q.organization_id == ^organization_id)
-
- @doc """
- Filters to beacons created by the given user.
- """
- @spec for_user(Ecto.Query.t(), Ecto.UUID.t()) :: Ecto.Query.t()
- def for_user(query, user_id), do: from(q in query, where: q.user_id == ^user_id)
-
- @doc """
- Filters to only enabled beacons.
- """
- @spec enabled(Ecto.Query.t()) :: Ecto.Query.t()
- def enabled(query), do: from(q in query, where: q.enabled == true)
-
- @doc """
- Filters to beacons of the given check type.
- """
- @spec of_type(Ecto.Query.t(), String.t()) :: Ecto.Query.t()
- def of_type(query, check_type), do: from(q in query, where: q.check_type == ^check_type)
-
- @doc """
- Orders beacons by name ascending.
- """
- @spec order_by_name(Ecto.Query.t()) :: Ecto.Query.t()
- def order_by_name(query), do: from(q in query, order_by: [asc: q.name])
-
- @doc """
- Preloads organization and user associations.
- """
- @spec preload_all(Ecto.Query.t()) :: Ecto.Query.t()
- def preload_all(query), do: from(q in query, preload: [:organization, :user])
-end
diff --git a/lib/towerops/snmp/discovery.ex b/lib/towerops/snmp/discovery.ex
index a14516f5..2786a1eb 100644
--- a/lib/towerops/snmp/discovery.ex
+++ b/lib/towerops/snmp/discovery.ex
@@ -229,7 +229,6 @@ defmodule Towerops.Snmp.Discovery do
{:ok, transceivers} = with_timeout(client_opts, fn -> Base.discover_transceivers(client_opts) end, timeouts)
Logger.info("Discovering printer supplies...", device_id: device.id)
-
{:ok, printer_supplies} =
with_timeout(client_opts, fn -> Base.discover_printer_supplies(client_opts) end, timeouts)
@@ -317,8 +316,7 @@ defmodule Towerops.Snmp.Discovery do
timeout: timeouts[:slow],
default: {:error, :interface_discovery_timeout}
) do
- {:ok, _ifaces} = ok ->
- ok
+ {:ok, _ifaces} = ok -> ok
{:error, :interface_discovery_timeout} ->
Logger.error("Interface discovery timed out - aborting to prevent data loss", device_id: device_id)
@@ -336,8 +334,7 @@ defmodule Towerops.Snmp.Discovery do
timeout: timeouts[:slow],
default: {:error, :sensor_discovery_timeout}
) do
- {:ok, _sensors} = ok ->
- ok
+ {:ok, _sensors} = ok -> ok
{:error, :sensor_discovery_timeout} ->
Logger.error("Sensor discovery timed out - aborting to prevent data loss", device_id: device_id)
@@ -1607,7 +1604,8 @@ defmodule Towerops.Snmp.Discovery do
@spec update_device_name_from_snmp(DeviceSchema.t(), system_info()) ::
{:ok, DeviceSchema.t()} | {:error, term()}
- defp update_device_name_from_snmp(%{name: name} = device, _system_info) when is_binary(name) and name != "" do
+ defp update_device_name_from_snmp(%{name: name} = device, _system_info)
+ when is_binary(name) and name != "" do
Logger.info("Device name already set or no sysName, skipping update", device_id: device.id)
{:ok, device}
end
diff --git a/lib/towerops_web/components/layouts.ex b/lib/towerops_web/components/layouts.ex
index eaa523c2..8f608689 100644
--- a/lib/towerops_web/components/layouts.ex
+++ b/lib/towerops_web/components/layouts.ex
@@ -378,12 +378,6 @@ defmodule ToweropsWeb.Layouts do
icon="hero-signal"
label={t("Coverage")}
/>
- <.sidebar_link
- navigate={~p"/beacons"}
- active={@active_page == "beacons"}
- icon="hero-radio"
- label={t("Beacon Monitors")}
- />
<%!-- <.sidebar_link
navigate={~p"/weathermap"}
active={@active_page == "weathermap"}
@@ -595,9 +589,6 @@ defmodule ToweropsWeb.Layouts do
<.mobile_nav_link navigate={~p"/network-map"} active={@active_page == "network-map"}>
{t("Network Map")}
- <.mobile_nav_link navigate={~p"/beacons"} active={@active_page == "beacons"}>
- {t("Beacon Monitors")}
-
<%!-- <.mobile_nav_link navigate={~p"/weathermap"} active={@active_page == "weathermap"}>
<.icon name="hero-signal" class="size-5" /> {t("Weathermap")}
--%>
@@ -939,12 +930,6 @@ defmodule ToweropsWeb.Layouts do
>
Exceptions
- <.link
- navigate={~p"/admin/beacons"}
- class="inline-flex items-center px-1 pt-1 pb-4 text-sm font-medium text-gray-700 hover:text-gray-900 dark:text-gray-300 dark:hover:text-white"
- >
- {t("Beacon Monitors")}
-
@@ -974,9 +959,6 @@ defmodule ToweropsWeb.Layouts do
<.mobile_nav_link navigate={~p"/admin/errors"} active={false}>
Exceptions
- <.mobile_nav_link navigate={~p"/admin/beacons"} active={false}>
- {t("Beacon Monitors")}
-
diff --git a/lib/towerops_web/live/admin/beacon_monitor_live/index.ex b/lib/towerops_web/live/admin/beacon_monitor_live/index.ex
deleted file mode 100644
index f17af46d..00000000
--- a/lib/towerops_web/live/admin/beacon_monitor_live/index.ex
+++ /dev/null
@@ -1,115 +0,0 @@
-defmodule ToweropsWeb.Admin.BeaconMonitorLive.Index do
- @moduledoc false
- use ToweropsWeb, :live_view
-
- import ToweropsWeb.BeaconMonitorLive.Helpers
- import ToweropsWeb.GettextHelpers
-
- alias Towerops.Beacons
- alias Towerops.Beacons.BeaconMonitor
-
- on_mount {ToweropsWeb.UserAuth, :require_superuser}
-
- @impl true
- def mount(_params, _session, socket) do
- assign_mount_state(socket, connected?(socket))
- end
-
- defp assign_mount_state(socket, true = _connected) do
- :ok = Phoenix.PubSub.subscribe(Towerops.PubSub, "beacons:*")
- {:ok, ref} = :timer.send_interval(60_000, :tick)
-
- socket
- |> assign_common()
- |> assign(:timer_ref, ref)
- |> then(&{:ok, &1})
- end
-
- defp assign_mount_state(socket, _not_connected) do
- socket
- |> assign_common()
- |> assign(:timer_ref, nil)
- |> then(&{:ok, &1})
- end
-
- defp assign_common(socket) do
- beacons = Beacons.list_all_beacons()
-
- socket
- |> assign(:page_title, t("All Beacons"))
- |> assign(:now, DateTime.utc_now())
- |> assign(:timezone, socket.assigns.current_scope.timezone)
- |> stream(:beacons, beacons, reset: true)
- |> assign(:has_beacons, beacons != [])
- end
-
- @impl true
- def handle_params(_params, _url, socket) do
- {:noreply, socket}
- end
-
- @impl true
- def handle_event("toggle_beacon", %{"id" => id}, socket) do
- socket =
- with %BeaconMonitor{} = beacon <- Beacons.get_beacon(id),
- {:ok, _updated} <- Beacons.toggle_beacon(beacon) do
- reload_beacons(socket)
- else
- _error -> put_flash(socket, :error, t_equipment("Beacon not found"))
- end
-
- {:noreply, socket}
- end
-
- @impl true
- def handle_event("delete_beacon", %{"id" => id}, socket) do
- socket =
- with %BeaconMonitor{} = beacon <- Beacons.get_beacon(id),
- {:ok, _deleted} <- Beacons.delete_beacon(beacon) do
- socket
- |> put_flash(:info, t_equipment("Beacon deleted successfully"))
- |> reload_beacons()
- else
- _error -> put_flash(socket, :error, t_equipment("Failed to delete beacon"))
- end
-
- {:noreply, socket}
- end
-
- @impl true
- def handle_info(:tick, socket) do
- {:noreply, assign(socket, :now, DateTime.utc_now())}
- end
-
- @impl true
- def handle_info({:beacon_updated, _beacon_id}, socket) do
- {:noreply, reload_beacons(socket)}
- end
-
- @impl true
- def handle_info({:beacon_created, _beacon_id}, socket) do
- {:noreply, reload_beacons(socket)}
- end
-
- @impl true
- def handle_info({:beacon_deleted, _beacon_id}, socket) do
- {:noreply, reload_beacons(socket)}
- end
-
- defp reload_beacons(socket) do
- beacons = Beacons.list_all_beacons()
-
- socket
- |> stream(:beacons, beacons, reset: true)
- |> assign(:has_beacons, beacons != [])
- end
-
- @impl true
- def terminate(_reason, socket) do
- _ = cancel_timer(socket.assigns[:timer_ref])
- :ok
- end
-
- defp cancel_timer(nil), do: :ok
- defp cancel_timer(ref), do: :timer.cancel(ref)
-end
diff --git a/lib/towerops_web/live/admin/beacon_monitor_live/index.html.heex b/lib/towerops_web/live/admin/beacon_monitor_live/index.html.heex
deleted file mode 100644
index 37ef7cd6..00000000
--- a/lib/towerops_web/live/admin/beacon_monitor_live/index.html.heex
+++ /dev/null
@@ -1,114 +0,0 @@
-
-
-
-
{t("All Beacons")}
-
- {t_equipment("Beacon monitors across all organizations")}
-
-
-
- <%= if !@has_beacons do %>
-
- <.icon name="hero-signal" class="mx-auto h-12 w-12 text-gray-400 dark:text-gray-500" />
-
- {t("No beacon monitors")}
-
-
- {t_equipment("No beacon monitors have been created yet.")}
-
-
- <% else %>
-
- <.table
- id="admin-beacons-table"
- rows={@streams.beacons}
- >
- <:col :let={{_id, beacon}} label={t_equipment("Name")}>
- {beacon.name}
-
-
- <:col :let={{_id, beacon}} label={t("Organization")}>
-
- {beacon.organization.name}
-
-
-
- <:col :let={{_id, beacon}} label={t_equipment("User")}>
-
- {beacon.user.email}
-
-
-
- <:col :let={{_id, beacon}} label={t_equipment("Check Type")}>
-
- {check_type_label(beacon.check_type)}
-
-
-
- <:col :let={{_id, beacon}} label={t_equipment("Target URL")}>
-
- {beacon.target_url}
-
-
-
- <:col :let={{_id, beacon}} label={t_equipment("Last Status")}>
-
-
- {status_label(beacon.last_status)}
-
-
-
- <:col :let={{_id, beacon}} label={t("Enabled")}>
-
-
-
- <:col :let={{_id, beacon}} label={t_equipment("Last Checked")}>
- <%= if beacon.last_checked_at do %>
- <.timestamp datetime={beacon.last_checked_at} timezone={@timezone} now={@now} />
- <% else %>
- {t_equipment("Never")}
- <% end %>
-
-
- <:action :let={{_id, beacon}}>
- <.button
- id={"admin-delete-beacon-#{beacon.id}"}
- type="button"
- phx-click="delete_beacon"
- phx-value-id={beacon.id}
- data-confirm={t_equipment("Are you sure you want to delete this beacon monitor?")}
- variant="danger"
- >
- {t("Delete")}
-
-
-
-
- <% end %>
-
-
diff --git a/lib/towerops_web/live/beacon_monitor_live/form_component.ex b/lib/towerops_web/live/beacon_monitor_live/form_component.ex
deleted file mode 100644
index ffc49756..00000000
--- a/lib/towerops_web/live/beacon_monitor_live/form_component.ex
+++ /dev/null
@@ -1,235 +0,0 @@
-defmodule ToweropsWeb.BeaconMonitorLive.FormComponent do
- @moduledoc false
- use ToweropsWeb, :live_component
-
- import ToweropsWeb.GettextHelpers
-
- alias Towerops.Beacons
- alias Towerops.Beacons.BeaconMonitor
-
- @check_type_options [
- {"HTTP", "http"},
- {"TCP", "tcp"},
- {"DNS", "dns"},
- {"SSL", "ssl"},
- {"ICMP", "icmp"}
- ]
-
- @impl true
- def render(assigns) do
- ~H"""
-
-
-
-
-
-
- <.form
- for={@form}
- id={"beacon-form-#{@action}"}
- phx-target={@myself}
- phx-change="validate"
- phx-submit="save"
- >
-
-
-
-
- {title_for_action(@action)}
-
-
- {subtitle_for_action(@action)}
-
-
-
-
-
-
- <.input
- field={@form[:name]}
- type="text"
- label={t_equipment("Name")}
- placeholder="e.g., Main Website"
- required
- />
-
- <.input
- field={@form[:check_type]}
- type="select"
- label={t_equipment("Check Type")}
- options={@check_type_options_formatted}
- />
-
- <.input
- field={@form[:target_url]}
- type="text"
- label={t_equipment("Target URL")}
- placeholder="https://example.com/health"
- required
- />
-
- <.input
- field={@form[:target_port]}
- type="number"
- label={t_equipment("Target Port")}
- placeholder="443"
- />
-
-
- <.input
- field={@form[:interval_seconds]}
- type="number"
- label={t_equipment("Interval (seconds)")}
- />
- <.input
- field={@form[:timeout_ms]}
- type="number"
- label={t_equipment("Timeout (ms)")}
- />
-
-
-
-
-
-
- <.button type="submit" variant="primary" phx-disable-with={t_equipment("Saving...")}>
- {submit_label(@action)}
-
-
-
-
-
-
- """
- end
-
- @impl true
- def mount(socket) do
- {:ok, socket}
- end
-
- @impl true
- def update(assigns, socket) do
- socket =
- socket
- |> assign(assigns)
- |> assign(:check_type_options_formatted, @check_type_options)
- |> assign_form()
- |> assign(:action, assigns[:action] || :new)
-
- {:ok, socket}
- end
-
- @impl true
- def handle_event("validate", %{"beacon_monitor" => params}, socket) do
- changeset =
- %BeaconMonitor{}
- |> BeaconMonitor.changeset(params)
- |> Map.put(:action, :validate)
-
- {:noreply, assign(socket, :form, to_form(changeset))}
- end
-
- @impl true
- def handle_event("save", %{"beacon_monitor" => params}, socket) do
- save_beacon(socket, socket.assigns.action, params)
- end
-
- @impl true
- def handle_event("close", _params, socket) do
- send(self(), {__MODULE__, :close})
- {:noreply, socket}
- end
-
- defp assign_form(socket) do
- beacon = socket.assigns[:beacon]
- action = socket.assigns[:action] || :new
-
- form_attrs = form_attrs_for_action(action, beacon)
-
- changeset =
- beacon
- |> beacon_or_new()
- |> BeaconMonitor.changeset(form_attrs)
-
- assign(socket, :form, to_form(changeset))
- end
-
- defp form_attrs_for_action(:edit, %BeaconMonitor{} = beacon) do
- %{
- "name" => beacon.name,
- "check_type" => beacon.check_type,
- "target_url" => beacon.target_url,
- "target_port" => beacon.target_port,
- "interval_seconds" => beacon.interval_seconds,
- "timeout_ms" => beacon.timeout_ms
- }
- end
-
- defp form_attrs_for_action(_action, _beacon) do
- %{"check_type" => "http", "interval_seconds" => 60, "timeout_ms" => 5000}
- end
-
- defp beacon_or_new(%BeaconMonitor{} = beacon), do: beacon
- defp beacon_or_new(_not_beacon), do: %BeaconMonitor{}
-
- defp save_beacon(socket, :edit, params) do
- beacon = socket.assigns.beacon
- organization_id = socket.assigns.current_scope.organization.id
- user_id = socket.assigns.current_scope.user.id
-
- params_with_ownership = Map.merge(params, %{"organization_id" => organization_id, "user_id" => user_id})
-
- case Beacons.update_beacon(beacon, params_with_ownership) do
- {:ok, _updated_beacon} ->
- send(self(), {__MODULE__, :saved})
- {:noreply, put_flash(socket, :info, t_equipment("Beacon updated successfully"))}
-
- {:error, %Ecto.Changeset{} = changeset} ->
- {:noreply, assign(socket, :form, to_form(changeset))}
- end
- end
-
- defp save_beacon(socket, _action, params) do
- organization_id = socket.assigns.current_scope.organization.id
- user_id = socket.assigns.current_scope.user.id
-
- params_with_ownership = Map.merge(params, %{"organization_id" => organization_id, "user_id" => user_id})
-
- case Beacons.create_beacon(params_with_ownership) do
- {:ok, _beacon} ->
- send(self(), {__MODULE__, :saved})
- {:noreply, put_flash(socket, :info, t_equipment("Beacon created successfully"))}
-
- {:error, %Ecto.Changeset{} = changeset} ->
- {:noreply, assign(socket, :form, to_form(changeset))}
- end
- end
-
- defp title_for_action(:edit), do: t_equipment("Edit Beacon Monitor")
- defp title_for_action(_action), do: t_equipment("Create Beacon Monitor")
-
- defp subtitle_for_action(:edit), do: t_equipment("Update the configuration for this beacon monitor")
- defp subtitle_for_action(_action), do: t_equipment("Configure a new beacon monitor to check your endpoint")
-
- defp submit_label(:edit), do: t_equipment("Save Changes")
- defp submit_label(_action), do: t_equipment("Create")
-end
diff --git a/lib/towerops_web/live/beacon_monitor_live/helpers.ex b/lib/towerops_web/live/beacon_monitor_live/helpers.ex
deleted file mode 100644
index bcb77ffd..00000000
--- a/lib/towerops_web/live/beacon_monitor_live/helpers.ex
+++ /dev/null
@@ -1,86 +0,0 @@
-defmodule ToweropsWeb.BeaconMonitorLive.Helpers do
- @moduledoc """
- Helper functions for Beacon Monitor LiveViews.
- """
-
- use Gettext, backend: ToweropsWeb.Gettext
-
- import ToweropsWeb.GettextHelpers
-
- @doc """
- Returns CSS classes for the check type badge based on check type.
- """
- def check_type_badge_class("http"),
- do: "bg-blue-50 text-blue-700 ring-blue-600/20 dark:bg-blue-900/30 dark:text-blue-300 dark:ring-blue-400/30"
-
- def check_type_badge_class("tcp"),
- do: "bg-green-50 text-green-700 ring-green-600/20 dark:bg-green-900/30 dark:text-green-300 dark:ring-green-400/30"
-
- def check_type_badge_class("dns"),
- do:
- "bg-purple-50 text-purple-700 ring-purple-600/20 dark:bg-purple-900/30 dark:text-purple-300 dark:ring-purple-400/30"
-
- def check_type_badge_class("ssl"),
- do: "bg-amber-50 text-amber-700 ring-amber-600/20 dark:bg-amber-900/30 dark:text-amber-300 dark:ring-amber-400/30"
-
- def check_type_badge_class("icmp"),
- do: "bg-rose-50 text-rose-700 ring-rose-600/20 dark:bg-rose-900/30 dark:text-rose-300 dark:ring-rose-400/30"
-
- def check_type_badge_class(_unknown),
- do: "bg-gray-50 text-gray-700 ring-gray-600/20 dark:bg-gray-900/30 dark:text-gray-300 dark:ring-gray-400/30"
-
- @doc """
- Returns the human-readable label for a check type.
- """
- def check_type_label("http"), do: "HTTP"
- def check_type_label("tcp"), do: "TCP"
- def check_type_label("dns"), do: "DNS"
- def check_type_label("ssl"), do: "SSL"
- def check_type_label("icmp"), do: "ICMP"
- def check_type_label(_unknown), do: t_equipment("Unknown")
-
- @doc """
- Returns CSS classes for the status badge based on last status.
- """
- def status_badge_class(nil),
- do: "bg-gray-50 text-gray-600 ring-gray-500/10 dark:bg-gray-400/10 dark:text-gray-400 dark:ring-gray-400/20"
-
- def status_badge_class("up"),
- do: "bg-green-50 text-green-700 ring-green-600/20 dark:bg-green-900/30 dark:text-green-300 dark:ring-green-400/30"
-
- def status_badge_class("down"),
- do: "bg-red-50 text-red-700 ring-red-600/20 dark:bg-red-900/30 dark:text-red-300 dark:ring-red-400/30"
-
- def status_badge_class(_unknown),
- do:
- "bg-yellow-50 text-yellow-700 ring-yellow-600/20 dark:bg-yellow-900/30 dark:text-yellow-300 dark:ring-yellow-400/30"
-
- @doc """
- Returns CSS classes for the status dot (a small colored circle).
- """
- def status_dot_class(nil), do: "bg-gray-400"
- def status_dot_class("up"), do: "bg-green-500"
- def status_dot_class("down"), do: "bg-red-500"
- def status_dot_class(_unknown), do: "bg-yellow-500"
-
- @doc """
- Returns the human-readable label for a beacon status.
- """
- def status_label(nil), do: t_equipment("Never checked")
- def status_label("up"), do: t_equipment("Up")
- def status_label("down"), do: t_equipment("Down")
- def status_label(unknown), do: unknown
-
- @doc """
- Formats a response time in milliseconds to a human-readable string.
- """
- def format_response_time(nil), do: "—"
-
- def format_response_time(ms) when ms < 1_000 do
- "#{Float.round(ms, 0)}ms"
- end
-
- def format_response_time(ms) when ms >= 1_000 do
- "#{Float.round(ms / 1000, 2)}s"
- end
-end
diff --git a/lib/towerops_web/live/beacon_monitor_live/index.ex b/lib/towerops_web/live/beacon_monitor_live/index.ex
deleted file mode 100644
index b9d2bf86..00000000
--- a/lib/towerops_web/live/beacon_monitor_live/index.ex
+++ /dev/null
@@ -1,180 +0,0 @@
-defmodule ToweropsWeb.BeaconMonitorLive.Index do
- @moduledoc false
- use ToweropsWeb, :live_view
-
- import ToweropsWeb.BeaconMonitorLive.Helpers
- import ToweropsWeb.GettextHelpers
-
- alias Towerops.Beacons
- alias Towerops.Beacons.BeaconMonitor
- alias ToweropsWeb.BeaconMonitorLive.FormComponent
-
- @impl true
- def mount(_params, _session, socket) do
- assign_mount_state(socket, connected?(socket))
- end
-
- defp assign_mount_state(socket, true = _connected) do
- organization = socket.assigns.current_scope.organization
-
- :ok = Phoenix.PubSub.subscribe(Towerops.PubSub, "beacons:#{organization.id}")
- {:ok, ref} = :timer.send_interval(60_000, :tick)
-
- socket
- |> assign_common()
- |> assign(:timer_ref, ref)
- |> then(&{:ok, &1})
- end
-
- defp assign_mount_state(socket, _not_connected) do
- socket
- |> assign_common()
- |> assign(:timer_ref, nil)
- |> then(&{:ok, &1})
- end
-
- defp assign_common(socket) do
- organization = socket.assigns.current_scope.organization
- current_user = socket.assigns.current_scope.user
-
- beacons = Beacons.list_user_beacons(organization.id, current_user.id)
-
- socket
- |> assign(:page_title, t("Beacon Monitors"))
- |> assign(:now, DateTime.utc_now())
- |> assign(:timezone, socket.assigns.current_scope.timezone)
- |> stream(:beacons, beacons, reset: true)
- |> assign(:has_beacons, beacons != [])
- end
-
- @impl true
- def handle_params(params, _url, socket) do
- socket = assign_modal_state(socket, socket.assigns.live_action, params)
-
- {:noreply, socket}
- end
-
- defp assign_modal_state(socket, :new, _params) do
- socket
- |> assign(:show_modal, true)
- |> assign(:modal_action, :new)
- |> assign(:modal_beacon, nil)
- end
-
- defp assign_modal_state(socket, :edit, %{"id" => id}) do
- organization = socket.assigns.current_scope.organization
- beacon = Beacons.get_beacon_for_org(id, organization.id)
-
- socket
- |> assign(:show_modal, true)
- |> assign(:modal_action, :edit)
- |> assign(:modal_beacon, beacon)
- end
-
- defp assign_modal_state(socket, _live_action, _params) do
- socket
- |> assign(:show_modal, false)
- |> assign(:modal_action, nil)
- |> assign(:modal_beacon, nil)
- end
-
- @impl true
- def handle_event("open_new", _params, socket) do
- {:noreply, push_navigate(socket, to: ~p"/beacons/new")}
- end
-
- @impl true
- def handle_event("open_edit", %{"id" => id}, socket) do
- {:noreply, push_navigate(socket, to: ~p"/beacons/#{id}/edit")}
- end
-
- @impl true
- def handle_event("toggle_beacon", %{"id" => id}, socket) do
- organization = socket.assigns.current_scope.organization
-
- socket =
- with %BeaconMonitor{} = beacon <- Beacons.get_beacon_for_org(id, organization.id),
- {:ok, _updated} <- Beacons.toggle_beacon(beacon) do
- reload_beacons(socket)
- else
- _error -> put_flash(socket, :error, t_equipment("Beacon not found"))
- end
-
- {:noreply, socket}
- end
-
- @impl true
- def handle_event("delete_beacon", %{"id" => id}, socket) do
- organization = socket.assigns.current_scope.organization
-
- socket =
- with %BeaconMonitor{} = beacon <- Beacons.get_beacon_for_org(id, organization.id),
- {:ok, _deleted} <- Beacons.delete_beacon(beacon) do
- socket
- |> put_flash(:info, t_equipment("Beacon deleted successfully"))
- |> reload_beacons()
- else
- _error -> put_flash(socket, :error, t_equipment("Failed to delete beacon"))
- end
-
- {:noreply, socket}
- end
-
- @impl true
- def handle_event("close_modal", _params, socket) do
- {:noreply, push_navigate(socket, to: ~p"/beacons")}
- end
-
- @impl true
- def handle_info(:tick, socket) do
- {:noreply, assign(socket, :now, DateTime.utc_now())}
- end
-
- @impl true
- def handle_info({FormComponent, :saved}, socket) do
- socket
- |> reload_beacons()
- |> push_navigate(to: ~p"/beacons")
- |> then(&{:noreply, &1})
- end
-
- @impl true
- def handle_info({FormComponent, :close}, socket) do
- {:noreply, push_navigate(socket, to: ~p"/beacons")}
- end
-
- @impl true
- def handle_info({:beacon_updated, _beacon_id}, socket) do
- {:noreply, reload_beacons(socket)}
- end
-
- @impl true
- def handle_info({:beacon_created, _beacon_id}, socket) do
- {:noreply, reload_beacons(socket)}
- end
-
- @impl true
- def handle_info({:beacon_deleted, _beacon_id}, socket) do
- {:noreply, reload_beacons(socket)}
- end
-
- defp reload_beacons(socket) do
- organization = socket.assigns.current_scope.organization
- current_user = socket.assigns.current_scope.user
-
- beacons = Beacons.list_user_beacons(organization.id, current_user.id)
-
- socket
- |> stream(:beacons, beacons, reset: true)
- |> assign(:has_beacons, beacons != [])
- end
-
- @impl true
- def terminate(_reason, socket) do
- _ = cancel_timer(socket.assigns[:timer_ref])
- :ok
- end
-
- defp cancel_timer(nil), do: :ok
- defp cancel_timer(ref), do: :timer.cancel(ref)
-end
diff --git a/lib/towerops_web/live/beacon_monitor_live/index.html.heex b/lib/towerops_web/live/beacon_monitor_live/index.html.heex
deleted file mode 100644
index 6feda721..00000000
--- a/lib/towerops_web/live/beacon_monitor_live/index.html.heex
+++ /dev/null
@@ -1,150 +0,0 @@
-
- <.header>
- {@page_title}
- <:subtitle>{t_equipment("Monitor your endpoints from distributed locations")}
- <:actions>
- <.button phx-click="open_new" variant="primary">
- <.icon name="hero-plus" class="h-5 w-5" /> {t("Add Beacon Monitor")}
-
-
-
-
- <%= if !@has_beacons do %>
-
- <.icon name="hero-signal" class="mx-auto h-12 w-12 text-gray-400 dark:text-gray-500" />
-
- {t("No beacon monitors")}
-
-
- {t_equipment("Get started by creating your first beacon monitor.")}
-
-
- <.button phx-click="open_new" variant="primary">
- <.icon name="hero-plus" class="h-5 w-5" /> {t("Add Beacon Monitor")}
-
-
-
- <% else %>
-
- <.table
- id="beacons-table"
- rows={@streams.beacons}
- >
- <:col :let={{_id, beacon}} label={t_equipment("Name")}>
-
{beacon.name}
-
-
- <:col :let={{_id, beacon}} label={t_equipment("Check Type")}>
-
- {check_type_label(beacon.check_type)}
-
-
-
- <:col :let={{_id, beacon}} label={t_equipment("Target URL")}>
-
- {beacon.target_url}
-
- <%= if beacon.check_type in ~w(tcp icmp) && beacon.target_port do %>
-
:{beacon.target_port}
- <% end %>
-
-
- <:col :let={{_id, beacon}} label={t_equipment("Last Status")}>
-
-
- {status_label(beacon.last_status)}
-
-
-
- <:col :let={{_id, beacon}} label={t_equipment("Response Time")}>
- <%= if beacon.last_response_time_ms do %>
-
- {format_response_time(beacon.last_response_time_ms)}
-
- <% else %>
-
—
- <% end %>
-
-
- <:col :let={{_id, beacon}} label={t_equipment("Last Checked")}>
- <%= if beacon.last_checked_at do %>
- <.timestamp datetime={beacon.last_checked_at} timezone={@timezone} now={@now} />
- <% else %>
-
{t_equipment("Never")}
- <% end %>
-
-
- <:col :let={{_id, beacon}} label={t("Enabled")}>
-
-
-
- <:action :let={{_id, beacon}}>
-
- <.button
- id={"edit-beacon-#{beacon.id}"}
- type="button"
- phx-click="open_edit"
- phx-value-id={beacon.id}
- variant="secondary"
- >
- {t("Edit")}
-
- <.button
- id={"delete-beacon-#{beacon.id}"}
- type="button"
- phx-click="delete_beacon"
- phx-value-id={beacon.id}
- data-confirm={t_equipment("Are you sure you want to delete this beacon monitor?")}
- variant="danger"
- >
- {t("Delete")}
-
-
-
-
-
- <% end %>
-
-
- <%= if @show_modal do %>
- <.live_component
- module={ToweropsWeb.BeaconMonitorLive.FormComponent}
- id={
- if @modal_action == :edit, do: "beacon-form-#{@modal_beacon.id}", else: "beacon-form-new"
- }
- action={@modal_action}
- beacon={@modal_beacon}
- current_scope={@current_scope}
- />
- <% end %>
-
diff --git a/lib/towerops_web/router.ex b/lib/towerops_web/router.ex
index de7eb688..73c7bf27 100644
--- a/lib/towerops_web/router.ex
+++ b/lib/towerops_web/router.ex
@@ -366,7 +366,6 @@ defmodule ToweropsWeb.Router do
live "/audit", AuditLive.Index, :index
live "/monitoring", MonitoringLive, :index
live "/agents", AgentLive.Index, :index
- live "/beacons", BeaconMonitorLive.Index, :index
live "/security", SecurityLive.Index, :index
end
end
@@ -463,11 +462,6 @@ defmodule ToweropsWeb.Router do
live "/agents/:id/edit", AgentLive.Edit, :edit
live "/agents/:id", AgentLive.Show, :show
- # Beacon Monitor routes
- live "/beacons", BeaconMonitorLive.Index, :index
- live "/beacons/new", BeaconMonitorLive.Index, :new
- live "/beacons/:id/edit", BeaconMonitorLive.Index, :edit
-
# Site routes
live "/sites", SiteLive.Index, :index
live "/sites/new", SiteLive.Form, :new
diff --git a/priv/gettext/en/LC_MESSAGES/default.po b/priv/gettext/en/LC_MESSAGES/default.po
index 950e6e0f..96641136 100644
--- a/priv/gettext/en/LC_MESSAGES/default.po
+++ b/priv/gettext/en/LC_MESSAGES/default.po
@@ -5455,26 +5455,3 @@ msgstr ""
#, elixir-autogen, elixir-format
msgid "— WISP Network Operator"
msgstr ""
-
-#: lib/towerops_web/live/beacon_monitor_live/index.ex:43
-#, elixir-autogen, elixir-format
-msgid "Beacon Monitors"
-msgstr "Beacon Monitors"
-
-#: lib/towerops_web/live/beacon_monitor_live/index.html.heex:12
-#: lib/towerops_web/live/beacon_monitor_live/index.html.heex:26
-#, elixir-autogen, elixir-format
-msgid "Add Beacon Monitor"
-msgstr "Add Beacon Monitor"
-
-#: lib/towerops_web/admin/beacon_monitor_live/index.ex:40
-#: lib/towerops_web/admin/beacon_monitor_live/index.html.heex:4
-#, elixir-autogen, elixir-format
-msgid "All Beacons"
-msgstr "All Beacons"
-
-#: lib/towerops_web/admin/beacon_monitor_live/index.html.heex:13
-#: lib/towerops_web/live/beacon_monitor_live/index.html.heex:20
-#, elixir-autogen, elixir-format
-msgid "No beacon monitors"
-msgstr "No beacon monitors"
diff --git a/priv/gettext/en/LC_MESSAGES/equipment.po b/priv/gettext/en/LC_MESSAGES/equipment.po
index ac5f7cb0..57b1f51c 100644
--- a/priv/gettext/en/LC_MESSAGES/equipment.po
+++ b/priv/gettext/en/LC_MESSAGES/equipment.po
@@ -318,176 +318,3 @@ msgstr "Please select exactly 2 backups to compare"
#, elixir-autogen, elixir-format
msgid "Unable to resolve alert"
msgstr ""
-
-#: lib/towerops_web/live/beacon_monitor_live/helpers.ex:69
-#, elixir-autogen, elixir-format
-msgid "Down"
-msgstr "Down"
-
-#: lib/towerops_web/live/beacon_monitor_live/helpers.ex:67
-#, elixir-autogen, elixir-format
-msgid "Never checked"
-msgstr "Never checked"
-
-#: lib/towerops_web/live/beacon_monitor_live/helpers.ex:68
-#, elixir-autogen, elixir-format
-msgid "Up"
-msgstr "Up"
-
-#: lib/towerops_web/live/beacon_monitor_live/helpers.ex:39
-#, elixir-autogen, elixir-format
-msgid "Unknown"
-msgstr "Unknown"
-
-#: lib/towerops_web/live/beacon_monitor_live/index.html.heex:22
-#, elixir-autogen, elixir-format
-msgid "Get started by creating your first beacon monitor."
-msgstr "Get started by creating your first beacon monitor."
-
-#: lib/towerops_web/live/beacon_monitor_live/index.html.heex:9
-#, elixir-autogen, elixir-format
-msgid "Monitor your endpoints from distributed locations"
-msgstr "Monitor your endpoints from distributed locations"
-
-#: lib/towerops_web/live/admin/beacon_monitor_live/index.html.heex:6
-#, elixir-autogen, elixir-format
-msgid "Beacon monitors across all organizations"
-msgstr "Beacon monitors across all organizations"
-
-#: lib/towerops_web/live/admin/beacon_monitor_live/index.html.heex:15
-#, elixir-autogen, elixir-format
-msgid "No beacon monitors have been created yet."
-msgstr "No beacon monitors have been created yet."
-
-#: lib/towerops_web/live/admin/beacon_monitor_live/index.ex:71
-#: lib/towerops_web/live/beacon_monitor_live/index.ex:116
-#, elixir-autogen, elixir-format
-msgid "Beacon deleted successfully"
-msgstr "Beacon deleted successfully"
-
-#: lib/towerops_web/live/admin/beacon_monitor_live/index.ex:59
-#: lib/towerops_web/live/beacon_monitor_live/index.ex:102
-#, elixir-autogen, elixir-format
-msgid "Beacon not found"
-msgstr "Beacon not found"
-
-#: lib/towerops_web/live/beacon_monitor_live/form_component.ex:199
-#, elixir-autogen, elixir-format
-msgid "Beacon updated successfully"
-msgstr "Beacon updated successfully"
-
-#: lib/towerops_web/live/beacon_monitor_live/form_component.ex:215
-#, elixir-autogen, elixir-format
-msgid "Beacon created successfully"
-msgstr "Beacon created successfully"
-
-#: lib/towerops_web/live/admin/beacon_monitor_live/index.ex:74
-#: lib/towerops_web/live/beacon_monitor_live/index.ex:119
-#, elixir-autogen, elixir-format
-msgid "Failed to delete beacon"
-msgstr "Failed to delete beacon"
-
-#: lib/towerops_web/live/admin/beacon_monitor_live/index.html.heex:99
-#: lib/towerops_web/live/beacon_monitor_live/index.html.heex:122
-#, elixir-autogen, elixir-format
-msgid "Are you sure you want to delete this beacon monitor?"
-msgstr "Are you sure you want to delete this beacon monitor?"
-
-#: lib/towerops_web/live/beacon_monitor_live/form_component.ex:223
-#, elixir-autogen, elixir-format
-msgid "Create Beacon Monitor"
-msgstr "Create Beacon Monitor"
-
-#: lib/towerops_web/live/beacon_monitor_live/form_component.ex:222
-#, elixir-autogen, elixir-format
-msgid "Edit Beacon Monitor"
-msgstr "Edit Beacon Monitor"
-
-#: lib/towerops_web/live/beacon_monitor_live/form_component.ex:226
-#, elixir-autogen, elixir-format
-msgid "Configure a new beacon monitor to check your endpoint"
-msgstr "Configure a new beacon monitor to check your endpoint"
-
-#: lib/towerops_web/live/beacon_monitor_live/form_component.ex:225
-#, elixir-autogen, elixir-format
-msgid "Update the configuration for this beacon monitor"
-msgstr "Update the configuration for this beacon monitor"
-
-#: lib/towerops_web/live/beacon_monitor_live/form_component.ex:229
-#, elixir-autogen, elixir-format
-msgid "Create"
-msgstr "Create"
-
-#: lib/towerops_web/live/beacon_monitor_live/form_component.ex:228
-#, elixir-autogen, elixir-format
-msgid "Save Changes"
-msgstr "Save Changes"
-
-#: lib/towerops_web/live/beacon_monitor_live/form_component.ex:89
-#, elixir-autogen, elixir-format
-msgid "Interval (seconds)"
-msgstr "Interval (seconds)"
-
-#: lib/towerops_web/live/beacon_monitor_live/form_component.ex:94
-#, elixir-autogen, elixir-format
-msgid "Timeout (ms)"
-msgstr "Timeout (ms)"
-
-#: lib/towerops_web/live/beacon_monitor_live/form_component.ex:81
-#, elixir-autogen, elixir-format
-msgid "Target Port"
-msgstr "Target Port"
-
-#: lib/towerops_web/live/admin/beacon_monitor_live/index.html.heex:49
-#: lib/towerops_web/live/beacon_monitor_live/form_component.ex:73
-#: lib/towerops_web/live/beacon_monitor_live/index.html.heex:49
-#, elixir-autogen, elixir-format
-msgid "Target URL"
-msgstr "Target URL"
-
-#: lib/towerops_web/live/admin/beacon_monitor_live/index.html.heex:40
-#: lib/towerops_web/live/beacon_monitor_live/form_component.ex:66
-#: lib/towerops_web/live/beacon_monitor_live/index.html.heex:40
-#, elixir-autogen, elixir-format
-msgid "Check Type"
-msgstr "Check Type"
-
-#: lib/towerops_web/live/admin/beacon_monitor_live/index.html.heex:85
-#: lib/towerops_web/live/beacon_monitor_live/index.html.heex:78
-#, elixir-autogen, elixir-format
-msgid "Last Checked"
-msgstr "Last Checked"
-
-#: lib/towerops_web/live/admin/beacon_monitor_live/index.html.heex:55
-#: lib/towerops_web/live/beacon_monitor_live/index.html.heex:58
-#, elixir-autogen, elixir-format
-msgid "Last Status"
-msgstr "Last Status"
-
-#: lib/towerops_web/live/beacon_monitor_live/index.html.heex:68
-#, elixir-autogen, elixir-format
-msgid "Response Time"
-msgstr "Response Time"
-
-#: lib/towerops_web/live/admin/beacon_monitor_live/index.html.heex:24
-#: lib/towerops_web/live/beacon_monitor_live/form_component.ex:58
-#: lib/towerops_web/live/beacon_monitor_live/index.html.heex:36
-#, elixir-autogen, elixir-format
-msgid "Name"
-msgstr "Name"
-
-#: lib/towerops_web/live/admin/beacon_monitor_live/index.html.heex:89
-#: lib/towerops_web/live/beacon_monitor_live/index.html.heex:82
-#, elixir-autogen, elixir-format
-msgid "Never"
-msgstr "Never"
-
-#: lib/towerops_web/live/beacon_monitor_live/form_component.ex:109
-#, elixir-autogen, elixir-format
-msgid "Saving..."
-msgstr "Saving..."
-
-#: lib/towerops_web/live/admin/beacon_monitor_live/index.html.heex:34
-#, elixir-autogen, elixir-format
-msgid "User"
-msgstr "User"
diff --git a/priv/repo/migrations/20260722124424_create_beacon_monitors.exs b/priv/repo/migrations/20260722124424_create_beacon_monitors.exs
deleted file mode 100644
index f0a95ee7..00000000
--- a/priv/repo/migrations/20260722124424_create_beacon_monitors.exs
+++ /dev/null
@@ -1,34 +0,0 @@
-defmodule Towerops.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 :name, :string, null: false
-
- add :organization_id, references(:organizations, type: :binary_id, on_delete: :delete_all),
- null: false
-
- add :user_id, references(:users, type: :binary_id, on_delete: :nilify_all), null: false
- add :check_type, :string, null: false, default: "http"
- add :target_url, :string, null: false
- add :target_port, :integer
- add :interval_seconds, :integer, null: false, default: 60
- add :timeout_ms, :integer, null: false, default: 5000
- add :config, :map, default: "{}"
- add :enabled, :boolean, null: false, default: true
- add :last_status, :string
- add :last_response_time_ms, :float
- add :last_checked_at, :utc_datetime_usec
- add :checks_count, :integer, null: false, default: 0
- add :failures_count, :integer, null: false, default: 0
-
- timestamps(type: :utc_datetime)
- end
-
- create index(:beacon_monitors, [:organization_id])
- create index(:beacon_monitors, [:user_id])
- create index(:beacon_monitors, [:check_type])
- create index(:beacon_monitors, [:enabled])
- end
-end
diff --git a/test/support/fixtures/beacons_fixtures.ex b/test/support/fixtures/beacons_fixtures.ex
deleted file mode 100644
index 37111649..00000000
--- a/test/support/fixtures/beacons_fixtures.ex
+++ /dev/null
@@ -1,27 +0,0 @@
-defmodule Towerops.BeaconsFixtures do
- @moduledoc """
- This module defines test helpers for creating
- entities via the `Towerops.Beacons` context.
- """
-
- @doc """
- Creates a beacon monitor fixture.
-
- Accepts optional attrs to override defaults.
- Requires `organization_id` and `user_id` to be provided.
- """
- def beacon_monitor_fixture(organization_id, user_id, attrs \\ %{}) do
- defaults = %{
- name: "Test Beacon #{System.unique_integer()}",
- check_type: "http",
- target_url: "https://example.com",
- organization_id: organization_id,
- user_id: user_id,
- interval_seconds: 60,
- timeout_ms: 5000
- }
-
- {:ok, beacon} = Towerops.Beacons.create_beacon(Map.merge(defaults, attrs))
- beacon
- end
-end
diff --git a/test/towerops/beacons_test.exs b/test/towerops/beacons_test.exs
deleted file mode 100644
index cd3e63e9..00000000
--- a/test/towerops/beacons_test.exs
+++ /dev/null
@@ -1,499 +0,0 @@
-defmodule Towerops.BeaconsTest do
- use Towerops.DataCase, async: true
-
- import Towerops.AccountsFixtures
- import Towerops.BeaconsFixtures
- import Towerops.OrganizationsFixtures
-
- alias Towerops.Beacons
- alias Towerops.Beacons.BeaconMonitor
-
- describe "list_organization_beacons/1" do
- test "returns beacons for organization" do
- user = user_fixture()
- org = organization_fixture(user.id)
- _beacon = beacon_monitor_fixture(org.id, user.id, %{name: "My Beacon"})
-
- beacons = Beacons.list_organization_beacons(org.id)
-
- assert length(beacons) == 1
- assert hd(beacons).name == "My Beacon"
- end
-
- test "returns empty list when no beacons" do
- user = user_fixture()
- org = organization_fixture(user.id)
-
- assert Beacons.list_organization_beacons(org.id) == []
- end
-
- test "does not return beacons from other organizations" do
- user = user_fixture()
- org1 = organization_fixture(user.id, %{name: "Org 1"})
- org2 = organization_fixture(user.id, %{name: "Org 2"})
-
- _beacon = beacon_monitor_fixture(org1.id, user.id)
-
- assert Beacons.list_organization_beacons(org2.id) == []
- end
- end
-
- describe "list_user_beacons/2" do
- test "returns only user's beacons" do
- user1 = user_fixture()
- user2 = user_fixture()
- org = organization_fixture(user1.id)
-
- _my_beacon = beacon_monitor_fixture(org.id, user1.id, %{name: "Mine"})
- _their_beacon = beacon_monitor_fixture(org.id, user2.id, %{name: "Theirs"})
-
- my_beacons = Beacons.list_user_beacons(org.id, user1.id)
-
- assert length(my_beacons) == 1
- assert hd(my_beacons).name == "Mine"
- end
-
- test "returns empty list when user has no beacons" do
- user1 = user_fixture()
- user2 = user_fixture()
- org = organization_fixture(user1.id)
-
- _beacon = beacon_monitor_fixture(org.id, user1.id)
-
- assert Beacons.list_user_beacons(org.id, user2.id) == []
- end
- end
-
- describe "list_all_beacons/0" do
- test "returns all beacons across orgs" do
- user = user_fixture()
- org1 = organization_fixture(user.id, %{name: "Org 1"})
- org2 = organization_fixture(user.id, %{name: "Org 2"})
-
- _b1 = beacon_monitor_fixture(org1.id, user.id)
- _b2 = beacon_monitor_fixture(org2.id, user.id)
-
- all = Beacons.list_all_beacons()
-
- assert length(all) >= 2
- end
-
- test "returns empty list when no beacons exist" do
- assert Beacons.list_all_beacons() == []
- end
- end
-
- describe "get_beacon/1" do
- test "returns beacon by id" do
- user = user_fixture()
- org = organization_fixture(user.id)
- beacon = beacon_monitor_fixture(org.id, user.id)
-
- assert %BeaconMonitor{} = Beacons.get_beacon(beacon.id)
- assert Beacons.get_beacon(beacon.id).id == beacon.id
- end
-
- test "returns nil for missing id" do
- assert Beacons.get_beacon(Ecto.UUID.generate()) == nil
- end
- end
-
- describe "get_beacon!/1" do
- test "returns beacon by id" do
- user = user_fixture()
- org = organization_fixture(user.id)
- beacon = beacon_monitor_fixture(org.id, user.id)
-
- assert %BeaconMonitor{} = Beacons.get_beacon!(beacon.id)
- assert Beacons.get_beacon!(beacon.id).id == beacon.id
- end
-
- test "raises for missing id" do
- assert_raise Ecto.NoResultsError, fn ->
- Beacons.get_beacon!(Ecto.UUID.generate())
- end
- end
- end
-
- describe "get_beacon_for_org/2" do
- test "returns beacon in org" do
- user = user_fixture()
- org = organization_fixture(user.id)
- beacon = beacon_monitor_fixture(org.id, user.id)
-
- assert %BeaconMonitor{} = Beacons.get_beacon_for_org(beacon.id, org.id)
- end
-
- test "returns nil for beacon in other org" do
- user = user_fixture()
- org1 = organization_fixture(user.id, %{name: "Org 1"})
- org2 = organization_fixture(user.id, %{name: "Org 2"})
- beacon = beacon_monitor_fixture(org1.id, user.id)
-
- assert Beacons.get_beacon_for_org(beacon.id, org2.id) == nil
- end
- end
-
- describe "get_beacon_for_user/2" do
- test "returns beacon for user" do
- user = user_fixture()
- org = organization_fixture(user.id)
- beacon = beacon_monitor_fixture(org.id, user.id)
-
- assert %BeaconMonitor{} = Beacons.get_beacon_for_user(beacon.id, user.id)
- end
-
- test "returns nil for beacon belonging to other user" do
- user1 = user_fixture()
- user2 = user_fixture()
- org = organization_fixture(user1.id)
- beacon = beacon_monitor_fixture(org.id, user1.id)
-
- assert Beacons.get_beacon_for_user(beacon.id, user2.id) == nil
- end
- end
-
- describe "create_beacon/1" do
- test "creates with valid attrs" do
- user = user_fixture()
- org = organization_fixture(user.id)
-
- attrs = %{
- name: "Test HTTP Monitor",
- check_type: "http",
- target_url: "https://example.com",
- interval_seconds: 30,
- timeout_ms: 3000,
- organization_id: org.id,
- user_id: user.id
- }
-
- assert {:ok, %BeaconMonitor{} = beacon} = Beacons.create_beacon(attrs)
- assert beacon.name == "Test HTTP Monitor"
- assert beacon.check_type == "http"
- assert beacon.target_url == "https://example.com"
- assert beacon.interval_seconds == 30
- assert beacon.timeout_ms == 3000
- assert beacon.enabled == true
- assert beacon.checks_count == 0
- assert beacon.failures_count == 0
- assert beacon.organization_id == org.id
- assert beacon.user_id == user.id
- end
-
- test "rejects invalid check_type" do
- user = user_fixture()
- org = organization_fixture(user.id)
-
- attrs = %{
- name: "Bad Check",
- check_type: "invalid",
- target_url: "https://example.com",
- interval_seconds: 60,
- timeout_ms: 5000,
- organization_id: org.id,
- user_id: user.id
- }
-
- assert {:error, %Ecto.Changeset{}} = Beacons.create_beacon(attrs)
- end
-
- test "requires name" do
- user = user_fixture()
- org = organization_fixture(user.id)
-
- attrs = %{
- check_type: "http",
- target_url: "https://example.com",
- interval_seconds: 60,
- timeout_ms: 5000,
- organization_id: org.id,
- user_id: user.id
- }
-
- assert {:error, changeset} = Beacons.create_beacon(attrs)
- assert "can't be blank" in errors_on(changeset).name
- end
-
- test "requires target_url" do
- user = user_fixture()
- org = organization_fixture(user.id)
-
- attrs = %{
- name: "No URL",
- check_type: "http",
- interval_seconds: 60,
- timeout_ms: 5000,
- organization_id: org.id,
- user_id: user.id
- }
-
- assert {:error, changeset} = Beacons.create_beacon(attrs)
- assert "can't be blank" in errors_on(changeset).target_url
- end
-
- test "accepts valid check types" do
- user = user_fixture()
- org = organization_fixture(user.id)
-
- for {check_type, extra_attrs} <- [
- {"http", %{}},
- {"dns", %{}},
- {"ssl", %{}},
- {"tcp", %{target_port: 443}},
- {"icmp", %{target_port: 7}}
- ] do
- attrs =
- Map.merge(
- %{
- name: "Test #{check_type}",
- check_type: check_type,
- target_url: "https://example.com",
- interval_seconds: 60,
- timeout_ms: 5000,
- organization_id: org.id,
- user_id: user.id
- },
- extra_attrs
- )
-
- assert {:ok, %BeaconMonitor{check_type: ^check_type}} =
- Beacons.create_beacon(attrs)
- end
- end
-
- test "rejects interval_seconds outside valid range" do
- user = user_fixture()
- org = organization_fixture(user.id)
-
- attrs = %{
- name: "Bad Interval",
- check_type: "http",
- target_url: "https://example.com",
- interval_seconds: 0,
- timeout_ms: 5000,
- organization_id: org.id,
- user_id: user.id
- }
-
- assert {:error, changeset} = Beacons.create_beacon(attrs)
- assert "must be greater than 0" in errors_on(changeset).interval_seconds
-
- attrs = %{
- name: "Bad Interval High",
- check_type: "http",
- target_url: "https://example.com",
- interval_seconds: 86_401,
- timeout_ms: 5000,
- organization_id: org.id,
- user_id: user.id
- }
-
- assert {:error, changeset} = Beacons.create_beacon(attrs)
- assert "must be less than 86401" in errors_on(changeset).interval_seconds
- end
-
- test "rejects timeout_ms outside valid range" do
- user = user_fixture()
- org = organization_fixture(user.id)
-
- attrs = %{
- name: "Bad Timeout",
- check_type: "http",
- target_url: "https://example.com",
- interval_seconds: 60,
- timeout_ms: 0,
- organization_id: org.id,
- user_id: user.id
- }
-
- assert {:error, changeset} = Beacons.create_beacon(attrs)
- assert "must be greater than 0" in errors_on(changeset).timeout_ms
-
- attrs = %{
- name: "Bad Timeout High",
- check_type: "http",
- target_url: "https://example.com",
- interval_seconds: 60,
- timeout_ms: 60_001,
- organization_id: org.id,
- user_id: user.id
- }
-
- assert {:error, changeset} = Beacons.create_beacon(attrs)
- assert "must be less than 60001" in errors_on(changeset).timeout_ms
- end
- end
-
- describe "update_beacon/2" do
- test "updates beacon name" do
- user = user_fixture()
- org = organization_fixture(user.id)
- beacon = beacon_monitor_fixture(org.id, user.id)
-
- assert {:ok, %BeaconMonitor{name: "Updated Name"}} =
- Beacons.update_beacon(beacon, %{name: "Updated Name"})
- end
-
- test "rejects invalid update" do
- user = user_fixture()
- org = organization_fixture(user.id)
- beacon = beacon_monitor_fixture(org.id, user.id)
-
- assert {:error, %Ecto.Changeset{}} =
- Beacons.update_beacon(beacon, %{check_type: "invalid"})
- end
-
- test "preserves unchanged fields" do
- user = user_fixture()
- org = organization_fixture(user.id)
-
- beacon =
- beacon_monitor_fixture(org.id, user.id, %{
- name: "Original",
- target_url: "https://original.com"
- })
-
- assert {:ok, updated} = Beacons.update_beacon(beacon, %{name: "Renamed"})
- assert updated.name == "Renamed"
- assert updated.target_url == "https://original.com"
- end
- end
-
- describe "delete_beacon/1" do
- test "deletes beacon" do
- user = user_fixture()
- org = organization_fixture(user.id)
- beacon = beacon_monitor_fixture(org.id, user.id)
-
- assert {:ok, %BeaconMonitor{}} = Beacons.delete_beacon(beacon)
- assert Beacons.get_beacon(beacon.id) == nil
- end
- end
-
- describe "toggle_beacon/1" do
- test "disables enabled beacon" do
- user = user_fixture()
- org = organization_fixture(user.id)
- beacon = beacon_monitor_fixture(org.id, user.id, %{enabled: true})
-
- assert {:ok, %BeaconMonitor{enabled: false}} = Beacons.toggle_beacon(beacon)
- end
-
- test "enables disabled beacon" do
- user = user_fixture()
- org = organization_fixture(user.id)
- beacon = beacon_monitor_fixture(org.id, user.id, %{enabled: false})
-
- assert {:ok, %BeaconMonitor{enabled: true}} = Beacons.toggle_beacon(beacon)
- end
-
- test "toggles idempotently back and forth" do
- user = user_fixture()
- org = organization_fixture(user.id)
- beacon = beacon_monitor_fixture(org.id, user.id, %{enabled: true})
-
- assert {:ok, %BeaconMonitor{enabled: false}} = Beacons.toggle_beacon(beacon)
-
- # Toggle again — re-fetch to get updated struct
- beacon = Beacons.get_beacon!(beacon.id)
- assert {:ok, %BeaconMonitor{enabled: true}} = Beacons.toggle_beacon(beacon)
- end
- end
-
- describe "record_check_result/3" do
- test "updates status and counters for up status" do
- user = user_fixture()
- org = organization_fixture(user.id)
- beacon = beacon_monitor_fixture(org.id, user.id)
-
- {:ok, updated} = Beacons.record_check_result(beacon, "up", 150.5)
-
- assert updated.last_status == "up"
- assert updated.last_response_time_ms == 150.5
- assert updated.checks_count == 1
- assert updated.failures_count == 0
- assert updated.last_checked_at
- end
-
- test "increments failures for down status" do
- user = user_fixture()
- org = organization_fixture(user.id)
- beacon = beacon_monitor_fixture(org.id, user.id)
-
- {:ok, updated} = Beacons.record_check_result(beacon, "down", 0.0)
-
- assert updated.last_status == "down"
- assert updated.failures_count == 1
- assert updated.checks_count == 1
- end
-
- test "increments failures for timeout status" do
- user = user_fixture()
- org = organization_fixture(user.id)
- beacon = beacon_monitor_fixture(org.id, user.id)
-
- {:ok, updated} = Beacons.record_check_result(beacon, "timeout", 5000.0)
-
- assert updated.last_status == "timeout"
- assert updated.failures_count == 1
- assert updated.checks_count == 1
- end
-
- test "increments failures for error status" do
- user = user_fixture()
- org = organization_fixture(user.id)
- beacon = beacon_monitor_fixture(org.id, user.id)
-
- {:ok, updated} = Beacons.record_check_result(beacon, "error", 0.0)
-
- assert updated.last_status == "error"
- assert updated.failures_count == 1
- assert updated.checks_count == 1
- end
-
- test "accumulates checks and failures over multiple calls" do
- user = user_fixture()
- org = organization_fixture(user.id)
- beacon = beacon_monitor_fixture(org.id, user.id)
-
- {:ok, beacon} = Beacons.record_check_result(beacon, "up", 10.0)
- assert beacon.checks_count == 1
- assert beacon.failures_count == 0
-
- {:ok, beacon} = Beacons.record_check_result(beacon, "down", 0.0)
- assert beacon.checks_count == 2
- assert beacon.failures_count == 1
-
- {:ok, beacon} = Beacons.record_check_result(beacon, "up", 15.0)
- assert beacon.checks_count == 3
- assert beacon.failures_count == 1
-
- {:ok, beacon} = Beacons.record_check_result(beacon, "down", 0.0)
- assert beacon.checks_count == 4
- assert beacon.failures_count == 2
- end
-
- test "updates last_checked_at timestamp" do
- user = user_fixture()
- org = organization_fixture(user.id)
- beacon = beacon_monitor_fixture(org.id, user.id)
-
- assert beacon.last_checked_at == nil
-
- {:ok, updated} = Beacons.record_check_result(beacon, "up", 10.0)
-
- assert updated.last_checked_at
- end
-
- test "accepts zero response time" do
- user = user_fixture()
- org = organization_fixture(user.id)
- beacon = beacon_monitor_fixture(org.id, user.id)
-
- {:ok, updated} = Beacons.record_check_result(beacon, "up", 0.0)
-
- assert updated.last_response_time_ms == 0.0
- end
- end
-end
diff --git a/test/towerops_web/live/beacon_monitor_live_test.exs b/test/towerops_web/live/beacon_monitor_live_test.exs
deleted file mode 100644
index baa923dc..00000000
--- a/test/towerops_web/live/beacon_monitor_live_test.exs
+++ /dev/null
@@ -1,284 +0,0 @@
-defmodule ToweropsWeb.BeaconMonitorLiveTest do
- use ToweropsWeb.ConnCase
-
- import Phoenix.LiveViewTest
- import Towerops.BeaconsFixtures
-
- alias Towerops.Beacons
-
- setup :register_and_log_in_user
-
- setup %{user: user} do
- {:ok, organization} =
- Towerops.Organizations.create_organization(%{name: "Test Org"}, user.id)
-
- %{organization: organization}
- end
-
- describe "Index" do
- test "lists user's beacon monitors", %{conn: conn, organization: org, user: user} do
- _my_beacon = beacon_monitor_fixture(org.id, user.id, %{name: "My HTTP Monitor"})
-
- {:ok, _view, html} = live(conn, ~p"/beacons")
-
- assert html =~ "My HTTP Monitor"
- end
-
- test "shows empty state when no beacons", %{conn: conn} do
- {:ok, _view, html} = live(conn, ~p"/beacons")
-
- assert html =~ "No beacon monitors"
- end
-
- test "does not show other user's beacons", %{conn: conn, organization: org} do
- other_user = Towerops.AccountsFixtures.user_fixture(enable_totp: true)
- _theirs = beacon_monitor_fixture(org.id, other_user.id, %{name: "Not Mine"})
-
- {:ok, _view, html} = live(conn, ~p"/beacons")
-
- refute html =~ "Not Mine"
- end
-
- test "requires authentication" do
- conn = build_conn()
- {:error, redirect} = live(conn, ~p"/beacons")
-
- assert {:redirect, %{to: path}} = redirect
- assert path == ~p"/users/log-in"
- end
- end
-
- describe "Create beacon" do
- test "creates a new beacon monitor via modal", %{conn: conn, organization: org, user: user} do
- {:ok, view, _html} = live(conn, ~p"/beacons/new")
-
- view
- |> form("#beacon-form-new",
- beacon_monitor: %{
- name: "New HTTP Check",
- check_type: "http",
- target_url: "https://example.com",
- interval_seconds: "30",
- timeout_ms: "3000"
- }
- )
- |> render_submit()
-
- assert_redirect(view, ~p"/beacons")
-
- # Verify the beacon was created in the database
- beacons = Beacons.list_user_beacons(org.id, user.id)
- assert Enum.any?(beacons, &(&1.name == "New HTTP Check" and &1.target_url == "https://example.com"))
- end
-
- test "validates required fields on submit", %{conn: conn} do
- {:ok, view, _html} = live(conn, ~p"/beacons/new")
-
- html =
- view
- |> form("#beacon-form-new",
- beacon_monitor: %{
- name: "",
- check_type: "http",
- target_url: "",
- interval_seconds: "",
- timeout_ms: ""
- }
- )
- |> render_submit()
-
- assert html =~ "can't be blank"
- end
-
- test "open_edit navigates to edit route", %{conn: conn, organization: org, user: user} do
- beacon = beacon_monitor_fixture(org.id, user.id, %{name: "Edit Me"})
-
- {:ok, view, _html} = live(conn, ~p"/beacons")
-
- view
- |> element("#edit-beacon-#{beacon.id}")
- |> render_click()
-
- assert_redirect(view, ~p"/beacons/#{beacon.id}/edit")
- end
- end
-
- describe "Edit beacon" do
- test "loads edit modal with existing beacon data", %{conn: conn, organization: org, user: user} do
- beacon = beacon_monitor_fixture(org.id, user.id, %{name: "Original Name"})
-
- {:ok, _view, html} = live(conn, ~p"/beacons/#{beacon.id}/edit")
-
- assert html =~ "Edit Beacon Monitor"
- assert html =~ "Original Name"
- end
-
- test "updates beacon name and redirects", %{conn: conn, organization: org, user: user} do
- beacon = beacon_monitor_fixture(org.id, user.id, %{name: "Old Name"})
-
- {:ok, view, _html} = live(conn, ~p"/beacons/#{beacon.id}/edit")
-
- view
- |> form("#beacon-form-edit",
- beacon_monitor: %{
- name: "Updated Name",
- check_type: "http",
- target_url: beacon.target_url,
- interval_seconds: to_string(beacon.interval_seconds),
- timeout_ms: to_string(beacon.timeout_ms)
- }
- )
- |> render_submit()
-
- assert_redirect(view, ~p"/beacons")
-
- updated = Beacons.get_beacon(beacon.id)
- assert updated.name == "Updated Name"
- end
- end
-
- describe "Toggle beacon" do
- test "disables an enabled beacon", %{conn: conn, organization: org, user: user} do
- beacon = beacon_monitor_fixture(org.id, user.id, %{enabled: true})
-
- {:ok, view, _html} = live(conn, ~p"/beacons")
-
- view
- |> element("#toggle-beacon-#{beacon.id}")
- |> render_click()
-
- refute Beacons.get_beacon(beacon.id).enabled
- end
-
- test "enables a disabled beacon", %{conn: conn, organization: org, user: user} do
- beacon = beacon_monitor_fixture(org.id, user.id, %{enabled: false})
-
- {:ok, view, _html} = live(conn, ~p"/beacons")
-
- view
- |> element("#toggle-beacon-#{beacon.id}")
- |> render_click()
-
- assert Beacons.get_beacon(beacon.id).enabled
- end
- end
-
- describe "Delete beacon" do
- test "deletes a beacon", %{conn: conn, organization: org, user: user} do
- beacon = beacon_monitor_fixture(org.id, user.id, %{name: "To Delete"})
-
- {:ok, view, _html} = live(conn, ~p"/beacons")
-
- view
- |> element("#delete-beacon-#{beacon.id}")
- |> render_click()
-
- html = render(view)
- assert html =~ "Beacon deleted successfully"
- refute Beacons.get_beacon(beacon.id)
- end
- end
-
- describe "Timer cleanup" do
- test "cleans up timer on LiveView terminate", %{conn: conn} do
- {:ok, view, _html} = live(conn, ~p"/beacons")
-
- assert render(view) =~ "Beacon Monitors"
-
- stop_live(view)
- end
-
- test "handles nil timer ref in terminate gracefully", %{conn: conn} do
- {:ok, view, _html} = live(conn, ~p"/beacons")
-
- stop_live(view)
- end
-
- defp stop_live(view) do
- Process.exit(view.pid, :kill)
- ref = Process.monitor(view.pid)
- assert_receive {:DOWN, ^ref, :process, _, _}, 1000
- end
- end
-
- describe "Admin index" do
- setup do
- user = Towerops.AccountsFixtures.user_fixture(enable_totp: true)
- user = user |> Ecto.Changeset.change(%{is_superuser: true}) |> Towerops.Repo.update!()
- {:ok, organization} = Towerops.Organizations.create_organization(%{name: "Test Org"}, user.id)
-
- token = Towerops.Accounts.generate_user_session_token(user)
-
- conn =
- build_conn()
- |> Phoenix.ConnTest.init_test_session(%{})
- |> Plug.Conn.put_session(:user_token, token)
-
- %{conn: conn, user: user, organization: organization}
- end
-
- test "lists all beacon monitors across orgs", %{conn: conn, user: user, organization: org} do
- other_user = Towerops.AccountsFixtures.user_fixture(enable_totp: false)
-
- {:ok, org2} = Towerops.Organizations.create_organization(%{name: "Org B"}, other_user.id, bypass_limits: true)
-
- _b1 = beacon_monitor_fixture(org.id, user.id, %{name: "Beacon A"})
- _b2 = beacon_monitor_fixture(org2.id, other_user.id, %{name: "Beacon B"})
-
- {:ok, _view, html} = live(conn, ~p"/admin/beacons")
- assert html =~ "Beacon A"
- assert html =~ "Beacon B"
- end
-
- test "redirects non-superuser", %{user: user} do
- regular_user =
- user
- |> Ecto.Changeset.change(%{is_superuser: false})
- |> Towerops.Repo.update!()
-
- token = Towerops.Accounts.generate_user_session_token(regular_user)
-
- conn =
- build_conn()
- |> Phoenix.ConnTest.init_test_session(%{})
- |> Plug.Conn.put_session(:user_token, token)
-
- assert {:error, {:redirect, %{to: "/orgs"}}} = live(conn, ~p"/admin/beacons")
- end
-
- test "requires authentication for admin" do
- conn = build_conn()
- assert {:error, {:redirect, %{to: "/users/log-in"}}} = live(conn, ~p"/admin/beacons")
- end
-
- test "admin can toggle beacon", %{conn: conn, user: user, organization: org} do
- beacon = beacon_monitor_fixture(org.id, user.id, %{enabled: true, name: "Admin Toggle"})
-
- {:ok, view, _html} = live(conn, ~p"/admin/beacons")
-
- view
- |> element("#admin-toggle-beacon-#{beacon.id}")
- |> render_click()
-
- refute Beacons.get_beacon(beacon.id).enabled
- end
-
- test "admin can delete beacon", %{conn: conn, user: user, organization: org} do
- beacon = beacon_monitor_fixture(org.id, user.id, %{name: "Admin Delete"})
-
- {:ok, view, _html} = live(conn, ~p"/admin/beacons")
-
- view
- |> element("#admin-delete-beacon-#{beacon.id}")
- |> render_click()
-
- refute Beacons.get_beacon(beacon.id)
- end
-
- test "shows empty state in admin when no beacons", %{conn: conn} do
- {:ok, _view, html} = live(conn, ~p"/admin/beacons")
-
- assert html =~ "No beacon monitors"
- end
- end
-end