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