Backend: - Migration: beacon_monitors table (binary_id PK, org/user FK, check_type, target_url, config, monitoring fields) - Schema: BeaconMonitor with changeset (pattern-matched port validation, check_type inclusion) - Query: BeaconMonitorQuery — composable scopes (by org, by user, by type, enabled, ordered, preloaded) - Context: Towerops.Beacons — CRUD, toggle, record_check_result (36/36 tests pass) Frontend: - User LiveView: /beacons — stream-based list, create/edit modals, toggle/delete actions - Admin LiveView: /admin/beacons — cross-org view with preloads (20/20 tests pass) - Form component: LiveComponent modal with validation - Helpers: check_type badges, status indicators, response time formatting - Router: user routes + admin route configured - Nav: sidebar + mobile links added - 30+ gettext translations Verification: compile ✓, format ✓, credo ✓, 56/56 new tests pass
180 lines
4.8 KiB
Elixir
180 lines
4.8 KiB
Elixir
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
|