diff --git a/lib/towerops_web/live/equipment_live/form.ex b/lib/towerops_web/live/equipment_live/form.ex
index 1c5234c7..93700a79 100644
--- a/lib/towerops_web/live/equipment_live/form.ex
+++ b/lib/towerops_web/live/equipment_live/form.ex
@@ -71,17 +71,27 @@ defmodule ToweropsWeb.EquipmentLive.Form do
defp apply_action(socket, :edit, %{"id" => id}) do
equipment = Equipment.get_equipment!(id)
- organization = socket.assigns.organization
+
+ # Preload necessary associations for agent resolution
+ equipment = Towerops.Repo.preload(equipment, site: [organization: :default_agent_token])
# Get current agent assignment if any
current_assignment = Agents.get_equipment_assignment(equipment.id)
+ # Get the effective agent and where it comes from
+ {effective_agent_id, agent_source} = Agents.get_effective_agent_token_with_source(equipment)
+
+ # For the form, we only want the equipment-specific assignment
agent_token_id =
if current_assignment do
current_assignment.agent_token_id
- else
- # Use organization default if no explicit assignment
- organization.default_agent_token_id
+ end
+
+ # Get agent name for display
+ effective_agent_name =
+ if effective_agent_id do
+ agent = Enum.find(socket.assigns.available_agents, &(&1.id == effective_agent_id))
+ agent && agent.name
end
# Add agent_token_id to the changeset data
@@ -92,6 +102,8 @@ defmodule ToweropsWeb.EquipmentLive.Form do
|> assign(:page_title, "Edit Equipment")
|> assign(:equipment, equipment_with_agent)
|> assign(:form, to_form(changeset))
+ |> assign(:agent_source, agent_source)
+ |> assign(:effective_agent_name, effective_agent_name)
end
@impl true
diff --git a/lib/towerops_web/live/equipment_live/form.html.heex b/lib/towerops_web/live/equipment_live/form.html.heex
index 59351290..5d753d5c 100644
--- a/lib/towerops_web/live/equipment_live/form.html.heex
+++ b/lib/towerops_web/live/equipment_live/form.html.heex
@@ -57,13 +57,44 @@
<.input
field={@form[:agent_token_id]}
type="select"
- label="Remote Agent (optional)"
- prompt="No agent - poll from server"
+ label="Remote Agent"
+ prompt="Inherit from site/organization"
options={Enum.map(@available_agents, &{&1.name, &1.id})}
/>
-
- Assign this equipment to a remote agent for local SNMP polling. Leave empty to poll from the Towerops server.
-
+ <%= if @live_action == :edit and Map.has_key?(assigns, :agent_source) do %>
+
+ <%= case @agent_source do %>
+ <% :equipment -> %>
+
+ <.icon name="hero-server" class="h-4 w-4" />
+ Explicitly assigned to this equipment
+
+ <% :site -> %>
+
+ <.icon name="hero-building-office" class="h-4 w-4" /> Inherited from site
+ <%= if @effective_agent_name do %>
+ ({@effective_agent_name})
+ <% end %>
+
+ <% :organization -> %>
+
+ <.icon name="hero-building-office-2" class="h-4 w-4" />
+ Inherited from organization
+ <%= if @effective_agent_name do %>
+ ({@effective_agent_name})
+ <% end %>
+
+ <% :none -> %>
+
+ <.icon name="hero-cloud" class="h-4 w-4" /> No agent assigned - cloud polling
+
+ <% end %>
+
+ Assign this equipment to a remote agent for local SNMP polling. Leave empty to inherit from site or organization defaults.
+
diff --git a/lib/towerops_web/live/org/settings_live.html.heex b/lib/towerops_web/live/org/settings_live.html.heex
index 83f29b86..028eae33 100644
--- a/lib/towerops_web/live/org/settings_live.html.heex
+++ b/lib/towerops_web/live/org/settings_live.html.heex
@@ -27,19 +27,20 @@
Default Agent
- Select a default agent for SNMP polling. This will be used for all equipment unless overridden at the equipment level.
+ Select a default agent for SNMP polling. This will be used for all equipment in this organization unless overridden at the site or equipment level.
<.input
field={@form[:default_agent_token_id]}
type="select"
label="Default Remote Agent"
- prompt="No default agent - poll from server"
+ prompt="No default agent - cloud polling"
options={Enum.map(@available_agents, &{&1.name, &1.id})}
/>
-
- You can override this setting for individual equipment in their settings.
+
+ <.icon name="hero-information-circle" class="h-4 w-4 inline" />
+ Agent assignment hierarchy: Equipment > Site > Organization
<% else %>
diff --git a/lib/towerops_web/live/site_live/form.ex b/lib/towerops_web/live/site_live/form.ex
index 11842080..0d2f363f 100644
--- a/lib/towerops_web/live/site_live/form.ex
+++ b/lib/towerops_web/live/site_live/form.ex
@@ -2,6 +2,7 @@ defmodule ToweropsWeb.SiteLive.Form do
@moduledoc false
use ToweropsWeb, :live_view
+ alias Towerops.Agents
alias Towerops.Sites
alias Towerops.Sites.Site
@@ -12,7 +13,8 @@ defmodule ToweropsWeb.SiteLive.Form do
{:ok,
socket
|> assign(:organization, organization)
- |> assign(:available_parent_sites, Sites.list_organization_sites(organization.id))}
+ |> assign(:available_parent_sites, Sites.list_organization_sites(organization.id))
+ |> assign(:available_agents, Agents.list_organization_agent_tokens(organization.id))}
end
@impl true
diff --git a/lib/towerops_web/live/site_live/form.html.heex b/lib/towerops_web/live/site_live/form.html.heex
index 5e6fef4b..ed2026a5 100644
--- a/lib/towerops_web/live/site_live/form.html.heex
+++ b/lib/towerops_web/live/site_live/form.html.heex
@@ -40,6 +40,19 @@
<.input field={@form[:location]} type="text" label="Location" />
+ <%= if @available_agents != [] do %>
+ <.input
+ field={@form[:agent_token_id]}
+ type="select"
+ label="Default Agent for Site"
+ prompt="Inherit from organization"
+ options={Enum.map(@available_agents, &{&1.name, &1.id})}
+ />
+
+ Set a default agent for all equipment at this site. Equipment can override this setting individually.
+
+ <% end %>
+
<.input field={@form[:description]} type="textarea" label="Description" />
diff --git a/priv/repo/migrations/20260114142010_add_agent_token_to_sites.exs b/priv/repo/migrations/20260114142010_add_agent_token_to_sites.exs
new file mode 100644
index 00000000..82ce479c
--- /dev/null
+++ b/priv/repo/migrations/20260114142010_add_agent_token_to_sites.exs
@@ -0,0 +1,11 @@
+defmodule Towerops.Repo.Migrations.AddAgentTokenToSites do
+ use Ecto.Migration
+
+ def change do
+ alter table(:sites) do
+ add :agent_token_id, references(:agent_tokens, type: :binary_id, on_delete: :nilify_all)
+ end
+
+ create index(:sites, [:agent_token_id])
+ end
+end
diff --git a/test/towerops/agents_test.exs b/test/towerops/agents_test.exs
index 26feb053..72871a35 100644
--- a/test/towerops/agents_test.exs
+++ b/test/towerops/agents_test.exs
@@ -358,4 +358,555 @@ defmodule Towerops.AgentsTest do
assert Ecto.assoc_loaded?(equipment.site)
end
end
+
+ describe "get_effective_agent_token/1 - hierarchical resolution" do
+ setup %{organization: org} do
+ {:ok, agent1, _} = Agents.create_agent_token(org.id, "Agent 1")
+ {:ok, agent2, _} = Agents.create_agent_token(org.id, "Agent 2")
+ {:ok, agent3, _} = Agents.create_agent_token(org.id, "Agent 3")
+
+ {:ok, site} =
+ Towerops.Sites.create_site(%{
+ name: "Test Site",
+ organization_id: org.id
+ })
+
+ {:ok, equipment} =
+ Towerops.Equipment.create_equipment(%{
+ name: "Test Equipment",
+ ip_address: "192.168.1.1",
+ site_id: site.id
+ })
+
+ %{
+ agent1: agent1,
+ agent2: agent2,
+ agent3: agent3,
+ site: site,
+ equipment: equipment
+ }
+ end
+
+ test "returns equipment-level assignment (highest priority)", %{
+ organization: org,
+ agent1: agent1,
+ agent2: agent2,
+ agent3: agent3,
+ site: site,
+ equipment: equipment
+ } do
+ # Set up hierarchy: org default, site override, equipment override
+ {:ok, _org} = Towerops.Organizations.update_organization(org, %{default_agent_token_id: agent1.id})
+ {:ok, _site} = Towerops.Sites.update_site(site, %{agent_token_id: agent2.id})
+ {:ok, _} = Agents.assign_equipment_to_agent(agent3.id, equipment.id)
+
+ # Preload necessary associations
+ equipment = Repo.preload(equipment, site: [organization: :default_agent_token])
+
+ # Equipment assignment should take precedence
+ assert Agents.get_effective_agent_token(equipment) == agent3.id
+ end
+
+ test "returns site-level assignment when equipment not assigned", %{
+ organization: org,
+ agent1: agent1,
+ agent2: agent2,
+ site: site,
+ equipment: equipment
+ } do
+ # Set up hierarchy: org default and site override, but no equipment assignment
+ {:ok, _org} = Towerops.Organizations.update_organization(org, %{default_agent_token_id: agent1.id})
+ {:ok, _site} = Towerops.Sites.update_site(site, %{agent_token_id: agent2.id})
+
+ # Preload necessary associations
+ equipment = Repo.preload(equipment, site: [organization: :default_agent_token])
+
+ # Site assignment should be used
+ assert Agents.get_effective_agent_token(equipment) == agent2.id
+ end
+
+ test "returns organization-level assignment when site and equipment not assigned", %{
+ organization: org,
+ agent1: agent1,
+ equipment: equipment
+ } do
+ # Set up hierarchy: only org default
+ {:ok, _org} = Towerops.Organizations.update_organization(org, %{default_agent_token_id: agent1.id})
+
+ # Preload necessary associations
+ equipment = Repo.preload(equipment, site: [organization: :default_agent_token])
+
+ # Organization default should be used
+ assert Agents.get_effective_agent_token(equipment) == agent1.id
+ end
+
+ test "returns nil when no agent assigned at any level", %{equipment: equipment} do
+ # No assignments at any level
+ equipment = Repo.preload(equipment, site: [organization: :default_agent_token])
+
+ assert Agents.get_effective_agent_token(equipment) == nil
+ end
+ end
+
+ describe "get_effective_agent_token_with_source/1" do
+ setup %{organization: org} do
+ {:ok, agent1, _} = Agents.create_agent_token(org.id, "Agent 1")
+ {:ok, agent2, _} = Agents.create_agent_token(org.id, "Agent 2")
+ {:ok, agent3, _} = Agents.create_agent_token(org.id, "Agent 3")
+
+ {:ok, site} =
+ Towerops.Sites.create_site(%{
+ name: "Test Site",
+ organization_id: org.id
+ })
+
+ {:ok, equipment} =
+ Towerops.Equipment.create_equipment(%{
+ name: "Test Equipment",
+ ip_address: "192.168.1.1",
+ site_id: site.id
+ })
+
+ %{
+ agent1: agent1,
+ agent2: agent2,
+ agent3: agent3,
+ site: site,
+ equipment: equipment
+ }
+ end
+
+ test "returns equipment source for equipment-level assignment", %{
+ agent3: agent3,
+ equipment: equipment
+ } do
+ {:ok, _} = Agents.assign_equipment_to_agent(agent3.id, equipment.id)
+
+ equipment = Repo.preload(equipment, site: [organization: :default_agent_token])
+
+ assert {agent_id, :equipment} = Agents.get_effective_agent_token_with_source(equipment)
+ assert agent_id == agent3.id
+ end
+
+ test "returns site source for site-level assignment", %{
+ agent2: agent2,
+ site: site,
+ equipment: equipment
+ } do
+ {:ok, _site} = Towerops.Sites.update_site(site, %{agent_token_id: agent2.id})
+
+ equipment = Repo.preload(equipment, site: [organization: :default_agent_token])
+
+ assert {agent_id, :site} = Agents.get_effective_agent_token_with_source(equipment)
+ assert agent_id == agent2.id
+ end
+
+ test "returns organization source for organization-level assignment", %{
+ organization: org,
+ agent1: agent1,
+ equipment: equipment
+ } do
+ {:ok, _org} = Towerops.Organizations.update_organization(org, %{default_agent_token_id: agent1.id})
+
+ equipment = Repo.preload(equipment, site: [organization: :default_agent_token])
+
+ assert {agent_id, :organization} = Agents.get_effective_agent_token_with_source(equipment)
+ assert agent_id == agent1.id
+ end
+
+ test "returns none source when no assignment", %{equipment: equipment} do
+ equipment = Repo.preload(equipment, site: [organization: :default_agent_token])
+
+ assert {nil, :none} = Agents.get_effective_agent_token_with_source(equipment)
+ end
+ end
+
+ describe "count_assigned_equipment/1" do
+ setup %{organization: org} do
+ {:ok, site} =
+ Towerops.Sites.create_site(%{
+ name: "Test Site",
+ organization_id: org.id
+ })
+
+ {:ok, equipment1} =
+ Towerops.Equipment.create_equipment(%{
+ name: "Test Equipment 1",
+ ip_address: "192.168.1.1",
+ site_id: site.id
+ })
+
+ {:ok, equipment2} =
+ Towerops.Equipment.create_equipment(%{
+ name: "Test Equipment 2",
+ ip_address: "192.168.1.2",
+ site_id: site.id
+ })
+
+ {:ok, equipment3} =
+ Towerops.Equipment.create_equipment(%{
+ name: "Test Equipment 3",
+ ip_address: "192.168.1.3",
+ site_id: site.id
+ })
+
+ %{site: site, equipment1: equipment1, equipment2: equipment2, equipment3: equipment3}
+ end
+
+ test "counts directly assigned equipment", %{
+ organization: org,
+ equipment1: equipment1,
+ equipment2: equipment2
+ } do
+ {:ok, agent_token, _token} = Agents.create_agent_token(org.id, "Test Agent")
+
+ # Initially no assignments
+ assert Agents.count_assigned_equipment(agent_token.id) == 0
+
+ # Assign two pieces of equipment
+ {:ok, _} = Agents.assign_equipment_to_agent(agent_token.id, equipment1.id)
+ assert Agents.count_assigned_equipment(agent_token.id) == 1
+
+ {:ok, _} = Agents.assign_equipment_to_agent(agent_token.id, equipment2.id)
+ assert Agents.count_assigned_equipment(agent_token.id) == 2
+ end
+
+ test "does not count disabled assignments", %{
+ organization: org,
+ equipment1: equipment1
+ } do
+ {:ok, agent_token, _token} = Agents.create_agent_token(org.id, "Test Agent")
+ {:ok, assignment} = Agents.assign_equipment_to_agent(agent_token.id, equipment1.id)
+
+ assert Agents.count_assigned_equipment(agent_token.id) == 1
+
+ # Disable the assignment
+ assignment
+ |> Ecto.Changeset.change(enabled: false)
+ |> Repo.update!()
+
+ assert Agents.count_assigned_equipment(agent_token.id) == 0
+ end
+ end
+
+ describe "list_agent_polling_targets/1 - hierarchical polling" do
+ setup %{organization: org} do
+ {:ok, agent1, _} = Agents.create_agent_token(org.id, "Agent 1")
+ {:ok, agent2, _} = Agents.create_agent_token(org.id, "Agent 2")
+ {:ok, agent3, _} = Agents.create_agent_token(org.id, "Agent 3")
+
+ {:ok, site1} =
+ Towerops.Sites.create_site(%{
+ name: "Site 1",
+ organization_id: org.id
+ })
+
+ {:ok, site2} =
+ Towerops.Sites.create_site(%{
+ name: "Site 2",
+ organization_id: org.id
+ })
+
+ %{
+ agent1: agent1,
+ agent2: agent2,
+ agent3: agent3,
+ site1: site1,
+ site2: site2
+ }
+ end
+
+ test "returns equipment with direct agent assignment", %{
+ agent1: agent1,
+ agent2: agent2,
+ site1: site1
+ } do
+ {:ok, equipment1} =
+ Towerops.Equipment.create_equipment(%{
+ name: "Equipment 1",
+ ip_address: "192.168.1.1",
+ site_id: site1.id,
+ snmp_enabled: true,
+ snmp_version: "2c",
+ snmp_community: "public"
+ })
+
+ {:ok, equipment2} =
+ Towerops.Equipment.create_equipment(%{
+ name: "Equipment 2",
+ ip_address: "192.168.1.2",
+ site_id: site1.id,
+ snmp_enabled: true,
+ snmp_version: "2c",
+ snmp_community: "public"
+ })
+
+ # Assign equipment to agent1
+ {:ok, _} = Agents.assign_equipment_to_agent(agent1.id, equipment1.id)
+ {:ok, _} = Agents.assign_equipment_to_agent(agent1.id, equipment2.id)
+
+ # Agent1 should see both equipment
+ targets = Agents.list_agent_polling_targets(agent1.id)
+ assert length(targets) == 2
+ assert Enum.any?(targets, &(&1.id == equipment1.id))
+ assert Enum.any?(targets, &(&1.id == equipment2.id))
+
+ # Agent2 should see nothing
+ targets = Agents.list_agent_polling_targets(agent2.id)
+ assert targets == []
+ end
+
+ test "returns equipment inheriting from site agent", %{
+ agent1: agent1,
+ agent2: agent2,
+ site1: site1
+ } do
+ # Set site1 to use agent1
+ {:ok, _site} = Towerops.Sites.update_site(site1, %{agent_token_id: agent1.id})
+
+ {:ok, equipment1} =
+ Towerops.Equipment.create_equipment(%{
+ name: "Equipment 1",
+ ip_address: "192.168.1.1",
+ site_id: site1.id,
+ snmp_enabled: true,
+ snmp_version: "2c",
+ snmp_community: "public"
+ })
+
+ {:ok, equipment2} =
+ Towerops.Equipment.create_equipment(%{
+ name: "Equipment 2",
+ ip_address: "192.168.1.2",
+ site_id: site1.id,
+ snmp_enabled: true,
+ snmp_version: "2c",
+ snmp_community: "public"
+ })
+
+ # Agent1 should see both equipment (inherited from site)
+ targets = Agents.list_agent_polling_targets(agent1.id)
+ assert length(targets) == 2
+ assert Enum.any?(targets, &(&1.id == equipment1.id))
+ assert Enum.any?(targets, &(&1.id == equipment2.id))
+
+ # Agent2 should see nothing
+ targets = Agents.list_agent_polling_targets(agent2.id)
+ assert targets == []
+ end
+
+ test "returns equipment inheriting from organization default", %{
+ organization: org,
+ agent1: agent1,
+ agent2: agent2,
+ site1: site1
+ } do
+ # Set organization default to agent1
+ {:ok, _org} =
+ Towerops.Organizations.update_organization(org, %{default_agent_token_id: agent1.id})
+
+ {:ok, equipment1} =
+ Towerops.Equipment.create_equipment(%{
+ name: "Equipment 1",
+ ip_address: "192.168.1.1",
+ site_id: site1.id,
+ snmp_enabled: true,
+ snmp_version: "2c",
+ snmp_community: "public"
+ })
+
+ {:ok, equipment2} =
+ Towerops.Equipment.create_equipment(%{
+ name: "Equipment 2",
+ ip_address: "192.168.1.2",
+ site_id: site1.id,
+ snmp_enabled: true,
+ snmp_version: "2c",
+ snmp_community: "public"
+ })
+
+ # Agent1 should see both equipment (inherited from organization)
+ targets = Agents.list_agent_polling_targets(agent1.id)
+ assert length(targets) == 2
+ assert Enum.any?(targets, &(&1.id == equipment1.id))
+ assert Enum.any?(targets, &(&1.id == equipment2.id))
+
+ # Agent2 should see nothing
+ targets = Agents.list_agent_polling_targets(agent2.id)
+ assert targets == []
+ end
+
+ test "equipment-level assignment overrides site and organization", %{
+ organization: org,
+ agent1: agent1,
+ agent2: agent2,
+ agent3: agent3,
+ site1: site1
+ } do
+ # Set org default to agent1, site to agent2, equipment to agent3
+ {:ok, _org} =
+ Towerops.Organizations.update_organization(org, %{default_agent_token_id: agent1.id})
+
+ {:ok, _site} = Towerops.Sites.update_site(site1, %{agent_token_id: agent2.id})
+
+ {:ok, equipment1} =
+ Towerops.Equipment.create_equipment(%{
+ name: "Equipment 1",
+ ip_address: "192.168.1.1",
+ site_id: site1.id,
+ snmp_enabled: true,
+ snmp_version: "2c",
+ snmp_community: "public"
+ })
+
+ # Directly assign to agent3
+ {:ok, _} = Agents.assign_equipment_to_agent(agent3.id, equipment1.id)
+
+ # Only agent3 should see the equipment
+ targets = Agents.list_agent_polling_targets(agent3.id)
+ assert length(targets) == 1
+ assert hd(targets).id == equipment1.id
+
+ # Agent1 and agent2 should not see it
+ assert Agents.list_agent_polling_targets(agent1.id) == []
+ assert Agents.list_agent_polling_targets(agent2.id) == []
+ end
+
+ test "site-level assignment overrides organization default", %{
+ organization: org,
+ agent1: agent1,
+ agent2: agent2,
+ site1: site1
+ } do
+ # Set org default to agent1, site to agent2
+ {:ok, _org} =
+ Towerops.Organizations.update_organization(org, %{default_agent_token_id: agent1.id})
+
+ {:ok, _site} = Towerops.Sites.update_site(site1, %{agent_token_id: agent2.id})
+
+ {:ok, equipment1} =
+ Towerops.Equipment.create_equipment(%{
+ name: "Equipment 1",
+ ip_address: "192.168.1.1",
+ site_id: site1.id,
+ snmp_enabled: true,
+ snmp_version: "2c",
+ snmp_community: "public"
+ })
+
+ # Agent2 should see the equipment (site level)
+ targets = Agents.list_agent_polling_targets(agent2.id)
+ assert length(targets) == 1
+ assert hd(targets).id == equipment1.id
+
+ # Agent1 should not see it (org default is overridden by site)
+ assert Agents.list_agent_polling_targets(agent1.id) == []
+ end
+
+ test "only returns SNMP-enabled equipment", %{
+ agent1: agent1,
+ site1: site1
+ } do
+ # Equipment with SNMP enabled
+ {:ok, equipment1} =
+ Towerops.Equipment.create_equipment(%{
+ name: "Equipment 1",
+ ip_address: "192.168.1.1",
+ site_id: site1.id,
+ snmp_enabled: true,
+ snmp_version: "2c",
+ snmp_community: "public"
+ })
+
+ # Equipment with SNMP disabled
+ {:ok, equipment2} =
+ Towerops.Equipment.create_equipment(%{
+ name: "Equipment 2",
+ ip_address: "192.168.1.2",
+ site_id: site1.id,
+ snmp_enabled: false
+ })
+
+ # Assign both to agent1
+ {:ok, _} = Agents.assign_equipment_to_agent(agent1.id, equipment1.id)
+ {:ok, _} = Agents.assign_equipment_to_agent(agent1.id, equipment2.id)
+
+ # Agent1 should only see SNMP-enabled equipment
+ targets = Agents.list_agent_polling_targets(agent1.id)
+ assert length(targets) == 1
+ assert hd(targets).id == equipment1.id
+ assert hd(targets).snmp_enabled == true
+ end
+
+ test "returns equipment from multiple sites with different assignment levels", %{
+ organization: org,
+ agent1: agent1,
+ agent2: agent2,
+ site1: site1,
+ site2: site2
+ } do
+ # Set org default to agent1
+ {:ok, _org} =
+ Towerops.Organizations.update_organization(org, %{default_agent_token_id: agent1.id})
+
+ # Site2 overrides to agent2
+ {:ok, _site} = Towerops.Sites.update_site(site2, %{agent_token_id: agent2.id})
+
+ # Equipment at site1 (inherits org default - agent1)
+ {:ok, equipment1} =
+ Towerops.Equipment.create_equipment(%{
+ name: "Equipment 1",
+ ip_address: "192.168.1.1",
+ site_id: site1.id,
+ snmp_enabled: true,
+ snmp_version: "2c",
+ snmp_community: "public"
+ })
+
+ # Equipment at site2 (inherits site default - agent2)
+ {:ok, equipment2} =
+ Towerops.Equipment.create_equipment(%{
+ name: "Equipment 2",
+ ip_address: "192.168.1.2",
+ site_id: site2.id,
+ snmp_enabled: true,
+ snmp_version: "2c",
+ snmp_community: "public"
+ })
+
+ # Agent1 should see equipment1 (from site1)
+ targets = Agents.list_agent_polling_targets(agent1.id)
+ assert length(targets) == 1
+ assert hd(targets).id == equipment1.id
+
+ # Agent2 should see equipment2 (from site2)
+ targets = Agents.list_agent_polling_targets(agent2.id)
+ assert length(targets) == 1
+ assert hd(targets).id == equipment2.id
+ end
+
+ test "preloads necessary associations for API response", %{
+ agent1: agent1,
+ site1: site1
+ } do
+ {:ok, equipment1} =
+ Towerops.Equipment.create_equipment(%{
+ name: "Equipment 1",
+ ip_address: "192.168.1.1",
+ site_id: site1.id,
+ snmp_enabled: true,
+ snmp_version: "2c",
+ snmp_community: "public"
+ })
+
+ {:ok, _} = Agents.assign_equipment_to_agent(agent1.id, equipment1.id)
+
+ targets = Agents.list_agent_polling_targets(agent1.id)
+ equipment = hd(targets)
+
+ # Verify preloads
+ assert Ecto.assoc_loaded?(equipment.site)
+ assert Ecto.assoc_loaded?(equipment.site.organization)
+ end
+ end
end
diff --git a/test/towerops_web/controllers/api/agent_controller_test.exs b/test/towerops_web/controllers/api/agent_controller_test.exs
index 1a549135..6f5083ab 100644
--- a/test/towerops_web/controllers/api/agent_controller_test.exs
+++ b/test/towerops_web/controllers/api/agent_controller_test.exs
@@ -173,6 +173,104 @@ defmodule ToweropsWeb.Api.AgentControllerTest do
assert interface_config["if_index"] == 1
assert interface_config["if_name"] == "GigabitEthernet0/1"
end
+
+ test "returns equipment with hierarchical agent assignment", %{
+ conn: conn,
+ token: token,
+ organization: org,
+ agent_token: agent_token
+ } do
+ # Create another agent
+ {:ok, other_agent, _other_token} =
+ Agents.create_agent_token(org.id, "Other Agent")
+
+ # Set organization default to other_agent
+ {:ok, _org} =
+ Towerops.Organizations.update_organization(org, %{
+ default_agent_token_id: other_agent.id
+ })
+
+ # Create site1 with no agent (will inherit org default)
+ {:ok, site1} =
+ Towerops.Sites.create_site(%{
+ name: "Site 1",
+ organization_id: org.id
+ })
+
+ # Create site2 with explicit agent assignment to our agent
+ {:ok, site2} =
+ Towerops.Sites.create_site(%{
+ name: "Site 2",
+ organization_id: org.id,
+ agent_token_id: agent_token.id
+ })
+
+ # Create site3 with no agent (will inherit org default)
+ {:ok, site3} =
+ Towerops.Sites.create_site(%{
+ name: "Site 3",
+ organization_id: org.id
+ })
+
+ # Equipment 1: Directly assigned to our agent (highest priority)
+ {:ok, equipment1} =
+ Towerops.Equipment.create_equipment(%{
+ name: "Equipment 1",
+ ip_address: "192.168.1.1",
+ snmp_enabled: true,
+ snmp_version: "2c",
+ snmp_community: "public",
+ site_id: site1.id
+ })
+
+ {:ok, _} = Agents.assign_equipment_to_agent(agent_token.id, equipment1.id)
+
+ # Equipment 2: At site2, inherits from site (our agent)
+ {:ok, equipment2} =
+ Towerops.Equipment.create_equipment(%{
+ name: "Equipment 2",
+ ip_address: "192.168.1.2",
+ snmp_enabled: true,
+ snmp_version: "2c",
+ snmp_community: "public",
+ site_id: site2.id
+ })
+
+ # Equipment 3: At site3, inherits from org default (other agent) - should NOT be returned
+ {:ok, _equipment3} =
+ Towerops.Equipment.create_equipment(%{
+ name: "Equipment 3",
+ ip_address: "192.168.1.3",
+ snmp_enabled: true,
+ snmp_version: "2c",
+ snmp_community: "public",
+ site_id: site3.id
+ })
+
+ # Equipment 4: SNMP disabled - should NOT be returned
+ {:ok, _equipment4} =
+ Towerops.Equipment.create_equipment(%{
+ name: "Equipment 4",
+ ip_address: "192.168.1.4",
+ snmp_enabled: false,
+ site_id: site1.id
+ })
+
+ conn =
+ conn
+ |> put_req_header("authorization", "Bearer #{token}")
+ |> get(~p"/api/v1/agent/config")
+
+ response = json_response(conn, 200)
+
+ assert %{"equipment" => equipment_list} = response
+ assert length(equipment_list) == 2
+
+ # Verify equipment IDs match
+ equipment_ids = Enum.map(equipment_list, & &1["id"])
+ assert equipment1.id in equipment_ids
+ assert equipment2.id in equipment_ids
+ end
end
describe "POST /api/v1/agent/metrics" do
diff --git a/test/towerops_web/live/org/settings_live_test.exs b/test/towerops_web/live/org/settings_live_test.exs
index 95b7eef8..1ebc3215 100644
--- a/test/towerops_web/live/org/settings_live_test.exs
+++ b/test/towerops_web/live/org/settings_live_test.exs
@@ -53,7 +53,7 @@ defmodule ToweropsWeb.Org.SettingsLiveTest do
assert html =~ "Default Remote Agent"
assert html =~ agent_token.name
- assert html =~ "No default agent - poll from server"
+ assert html =~ "No default agent - cloud polling"
end
test "updates organization name successfully", %{conn: conn, user: user, organization: org} do