towerops/lib/towerops_web/live/site_live/form.ex
Graham McIntire a2d96f8e6e
Implement hierarchical agent assignment for SNMP polling
Add three-level agent assignment hierarchy (Equipment > Site > Organization)
allowing flexible agent deployment strategies. Agents now receive only equipment
assigned to them through direct assignment or inheritance from site/organization
defaults.

Key changes:
- Add agent_token_id to Sites table with migration
- Implement get_effective_agent_token/1 for hierarchical resolution
- Add list_agent_polling_targets/1 to return polling targets per agent
- Update API config endpoint to use hierarchical polling targets
- Add agent assignment UI to equipment, site, and organization forms
- Show agent source (direct/site/org/none) in equipment form
- Add equipment count column to agent list
- Update terminology from "poll from server" to "cloud polling"

Tests:
- Add 8 comprehensive tests for list_agent_polling_targets/1
- Add end-to-end test for hierarchical config endpoint
- All 775 tests passing
2026-01-14 08:38:50 -06:00

105 lines
3.2 KiB
Elixir

defmodule ToweropsWeb.SiteLive.Form do
@moduledoc false
use ToweropsWeb, :live_view
alias Towerops.Agents
alias Towerops.Sites
alias Towerops.Sites.Site
@impl true
def mount(_params, _session, socket) do
organization = socket.assigns.current_organization
{:ok,
socket
|> assign(:organization, organization)
|> assign(:available_parent_sites, Sites.list_organization_sites(organization.id))
|> assign(:available_agents, Agents.list_organization_agent_tokens(organization.id))}
end
@impl true
def handle_params(params, _url, socket) do
{:noreply, apply_action(socket, socket.assigns.live_action, params)}
end
defp apply_action(socket, :new, _params) do
changeset = Sites.change_site(%Site{organization_id: socket.assigns.organization.id})
socket
|> assign(:page_title, "New Site")
|> assign(:site, %Site{})
|> assign(:form, to_form(changeset))
end
defp apply_action(socket, :edit, %{"id" => id}) do
site = Sites.get_organization_site!(socket.assigns.organization.id, id)
changeset = Sites.change_site(site)
# Filter out the current site and its descendants from parent options
available_parent_sites =
Enum.reject(socket.assigns.available_parent_sites, fn s -> s.id == site.id end)
socket
|> assign(:page_title, "Edit Site")
|> assign(:site, site)
|> assign(:form, to_form(changeset))
|> assign(:available_parent_sites, available_parent_sites)
end
@impl true
def handle_event("validate", %{"site" => site_params}, socket) do
changeset =
socket.assigns.site
|> Sites.change_site(site_params)
|> Map.put(:action, :validate)
{:noreply, assign(socket, :form, to_form(changeset))}
end
@impl true
def handle_event("save", %{"site" => site_params}, socket) do
save_site(socket, socket.assigns.live_action, site_params)
end
@impl true
def handle_event("delete", _params, socket) do
case Sites.delete_site(socket.assigns.site) do
{:ok, _} ->
{:noreply,
socket
|> put_flash(:info, "Site deleted successfully")
|> push_navigate(to: ~p"/orgs/#{socket.assigns.organization.slug}/sites")}
{:error, _} ->
{:noreply, put_flash(socket, :error, "Unable to delete site")}
end
end
defp save_site(socket, :new, site_params) do
site_params = Map.put(site_params, "organization_id", socket.assigns.organization.id)
case Sites.create_site(site_params) do
{:ok, site} ->
{:noreply,
socket
|> put_flash(:info, "Site created successfully! Now add your first device.")
|> push_navigate(to: ~p"/orgs/#{socket.assigns.organization.slug}/sites/#{site.id}")}
{:error, %Ecto.Changeset{} = changeset} ->
{:noreply, assign(socket, :form, to_form(changeset))}
end
end
defp save_site(socket, :edit, site_params) do
case Sites.update_site(socket.assigns.site, site_params) do
{:ok, _site} ->
{:noreply,
socket
|> put_flash(:info, "Site updated successfully")
|> push_navigate(to: ~p"/orgs/#{socket.assigns.organization.slug}/sites")}
{:error, %Ecto.Changeset{} = changeset} ->
{:noreply, assign(socket, :form, to_form(changeset))}
end
end
end