defmodule ToweropsWeb.AgentLive.Index do @moduledoc false use ToweropsWeb, :live_view use Gettext, backend: ToweropsWeb.Gettext import ToweropsWeb.AgentLive.Helpers import ToweropsWeb.GettextHelpers alias Towerops.Accounts.Scope alias Towerops.Agents alias Towerops.Agents.Stats alias Towerops.Settings @impl true def mount(_params, _session, socket) do organization = socket.assigns.current_scope.organization current_scope = socket.assigns.current_scope is_superuser = Scope.superuser?(current_scope) agent_tokens = Agents.list_organization_agent_tokens(organization.id) # If superadmin (including when impersonating), also load cloud pollers and global default {cloud_pollers, global_default_cloud_poller_id} = if is_superuser do {Agents.list_cloud_pollers(), Settings.get_global_default_cloud_poller()} else {[], nil} end # device counts for each agent (both direct and total with inheritance) equipment_counts = calculate_device_counts(agent_tokens) # device counts for cloud pollers (if superuser) cloud_poller_counts = if is_superuser do calculate_device_counts(cloud_pollers) else %{} end # Get organization-wide agent health statistics agent_health_stats = Stats.get_organization_agent_health(organization.id) assignment_breakdown = Stats.get_device_assignment_breakdown(organization.id) offline_agents = Stats.get_offline_agents(organization.id) # Get agent image URL from config or use default agent_image = Application.get_env(:towerops, :agent_docker_image, "towerops/agent:latest") {:ok, socket |> assign(:page_title, "Remote Agents") |> assign(:timezone, socket.assigns.current_scope.timezone) |> assign(:is_superuser, is_superuser) |> assign(:agent_tokens, agent_tokens) |> assign(:cloud_pollers, cloud_pollers) |> assign(:global_default_cloud_poller_id, global_default_cloud_poller_id) |> assign(:selected_global_default, global_default_cloud_poller_id || "") |> assign(:device_counts, equipment_counts) |> assign(:cloud_poller_counts, cloud_poller_counts) |> assign(:agent_health_stats, agent_health_stats) |> assign(:assignment_breakdown, assignment_breakdown) |> assign(:offline_agents, offline_agents) |> assign(:agent_image, agent_image) |> assign(:new_token, nil) |> assign(:show_token_modal, false) |> assign(:agent_form, to_form(%{"name" => "", "is_cloud_poller" => false}))} end @impl true def handle_params(params, _url, socket) do {:noreply, apply_action(socket, socket.assigns.live_action, params)} end @impl true def handle_event("create_agent", params, socket) do {name, is_cloud_poller} = parse_agent_params(params) with :ok <- validate_cloud_poller_permission(socket.assigns.current_scope, is_cloud_poller), {:ok, agent_token, token} <- create_agent(socket.assigns.current_scope.organization, name, is_cloud_poller) do handle_agent_creation_success(socket, agent_token, token, is_cloud_poller) else {:error, :unauthorized} -> {:noreply, put_flash(socket, :error, t_equipment("Only superadmins can create cloud pollers"))} {:error, _changeset} -> error_message = if is_cloud_poller, do: t_equipment("Failed to create cloud poller"), else: t_equipment("Failed to create agent") {:noreply, put_flash(socket, :error, error_message)} end end @impl true def handle_event("close_token_modal", _params, socket) do {:noreply, socket |> assign(:show_token_modal, false) |> assign(:new_token, nil)} end @impl true def handle_event("show_setup", %{"id" => id}, socket) do agent_token = Agents.get_agent_token!(id) {:noreply, socket |> assign(:new_token, %{agent_token: agent_token, token: agent_token.token}) |> assign(:show_token_modal, true)} end @impl true def handle_event("regenerate_token", %{"id" => id}, socket) do agent_token = Agents.get_agent_token!(id) organization = socket.assigns.current_scope.organization # Revoke the old token and create a new one case Agents.revoke_agent_token(id) do {:ok, _} -> case Agents.create_agent_token(organization.id, agent_token.name) do {:ok, new_agent_token, token} -> agent_tokens = Agents.list_organization_agent_tokens(organization.id) {:noreply, socket |> assign(:agent_tokens, agent_tokens) |> assign(:new_token, %{agent_token: new_agent_token, token: token}) |> assign(:show_token_modal, true) |> put_flash(:info, t_equipment("New token generated successfully"))} {:error, _changeset} -> {:noreply, put_flash(socket, :error, t_equipment("Failed to generate new token"))} end {:error, _} -> {:noreply, put_flash(socket, :error, t_equipment("Failed to revoke old token"))} end end @impl true def handle_event("delete_agent", %{"id" => id}, socket) do case Agents.delete_agent_token(id) do {:ok, _} -> organization = socket.assigns.current_scope.organization # If the deleted agent was the global default, clear it if Scope.superuser?(socket.assigns.current_scope) && socket.assigns.global_default_cloud_poller_id == id do Settings.set_global_default_cloud_poller(nil) end agent_tokens = Agents.list_organization_agent_tokens(organization.id) # Refresh cloud pollers list if superadmin (in case a cloud poller was deleted) cloud_pollers = load_cloud_pollers_if_superuser(socket.assigns.current_scope) # Reload global default cloud poller ID global_default_cloud_poller_id = if Scope.superuser?(socket.assigns.current_scope) do Settings.get_global_default_cloud_poller() else socket.assigns.global_default_cloud_poller_id end # Recalculate device counts after deletion equipment_counts = Map.new(agent_tokens, fn token -> direct = Agents.count_assigned_devices(token.id) total = length(Agents.list_agent_polling_targets(token.id)) {token.id, %{direct: direct, total: total}} end) cloud_poller_counts = calculate_device_counts(cloud_pollers) # Refresh health statistics agent_health_stats = Stats.get_organization_agent_health(organization.id) assignment_breakdown = Stats.get_device_assignment_breakdown(organization.id) offline_agents = Stats.get_offline_agents(organization.id) {:noreply, socket |> assign(:agent_tokens, agent_tokens) |> assign(:cloud_pollers, cloud_pollers) |> assign(:global_default_cloud_poller_id, global_default_cloud_poller_id) |> assign(:selected_global_default, global_default_cloud_poller_id || "") |> assign(:device_counts, equipment_counts) |> assign(:cloud_poller_counts, cloud_poller_counts) |> assign(:agent_health_stats, agent_health_stats) |> assign(:assignment_breakdown, assignment_breakdown) |> assign(:offline_agents, offline_agents) |> put_flash( :info, t_equipment("Agent deleted successfully. Devices now fall back to site/org defaults or cloud polling.") )} {:error, _} -> {:noreply, put_flash(socket, :error, t_equipment("Failed to delete agent"))} end end @impl true def handle_event("update_selected_global_default", %{"agent_token_id" => agent_token_id}, socket) do # Just update the selected value in the dropdown, don't save yet {:noreply, assign(socket, :selected_global_default, agent_token_id)} end @impl true def handle_event("save_global_default", _params, socket) do current_scope = socket.assigns.current_scope if Scope.superuser?(current_scope) do agent_token_id = socket.assigns.selected_global_default # Handle empty string as nil agent_token_id = if agent_token_id == "", do: nil, else: agent_token_id # Validate that the agent exists if not nil case validate_and_save_global_default(agent_token_id) do {:ok, validated_id} -> handle_global_default_success(socket, validated_id) {:error, :agent_not_found} -> {:noreply, put_flash(socket, :error, t_equipment("Selected agent no longer exists. Please choose another agent."))} {:error, _} -> {:noreply, put_flash(socket, :error, t_equipment("Failed to update global default cloud poller"))} end else {:noreply, put_flash(socket, :error, t_equipment("Only superadmins can set the global default cloud poller"))} end end defp apply_action(socket, :index, _params) do socket end defp parse_agent_params(params) do case params do %{"agent_form" => %{"name" => n, "is_cloud_poller" => cp}} -> {n, cp in ["true", "on", true]} %{"agent_form" => %{"name" => n}} -> {n, false} %{"name" => n, "is_cloud_poller" => cp} -> {n, cp in ["true", "on", true]} %{"name" => n} -> {n, false} end end defp validate_cloud_poller_permission(scope, is_cloud_poller) do if is_cloud_poller && !Scope.superuser?(scope) do {:error, :unauthorized} else :ok end end defp create_agent(organization, name, is_cloud_poller) do if is_cloud_poller do Agents.create_cloud_poller(name) else Agents.create_agent_token(organization.id, name) end end defp handle_agent_creation_success(socket, agent_token, token, is_cloud_poller) do organization = socket.assigns.current_scope.organization agent_tokens = Agents.list_organization_agent_tokens(organization.id) cloud_pollers = load_cloud_pollers_if_superuser(socket.assigns.current_scope) equipment_counts = calculate_device_counts(agent_tokens) cloud_poller_counts = calculate_device_counts(cloud_pollers) agent_health_stats = Stats.get_organization_agent_health(organization.id) assignment_breakdown = Stats.get_device_assignment_breakdown(organization.id) offline_agents = Stats.get_offline_agents(organization.id) success_message = get_success_message(is_cloud_poller) {:noreply, socket |> assign(:agent_tokens, agent_tokens) |> assign(:cloud_pollers, cloud_pollers) |> assign(:device_counts, equipment_counts) |> assign(:cloud_poller_counts, cloud_poller_counts) |> assign(:agent_health_stats, agent_health_stats) |> assign(:assignment_breakdown, assignment_breakdown) |> assign(:offline_agents, offline_agents) |> assign(:new_token, %{agent_token: agent_token, token: token}) |> assign(:show_token_modal, true) |> assign(:agent_form, to_form(%{"name" => "", "is_cloud_poller" => false})) |> put_flash(:info, success_message)} end defp load_cloud_pollers_if_superuser(scope) do if Scope.superuser?(scope), do: Agents.list_cloud_pollers(), else: [] end defp calculate_device_counts(agent_tokens) do Map.new(agent_tokens, fn t -> direct = Agents.count_assigned_devices(t.id) total = length(Agents.list_agent_polling_targets(t.id)) {t.id, %{direct: direct, total: total}} end) end defp get_success_message(is_cloud_poller) do if is_cloud_poller, do: t_equipment("Cloud poller created successfully"), else: t_equipment("Agent created successfully") end defp validate_and_save_global_default(nil) do # Clearing the default is always valid case Settings.set_global_default_cloud_poller(nil) do {:ok, _} -> {:ok, nil} error -> error end end defp validate_and_save_global_default(agent_token_id) do # Verify the agent exists before saving %Agents.AgentToken{} = Agents.get_agent_token!(agent_token_id) case Settings.set_global_default_cloud_poller(agent_token_id) do {:ok, _} -> {:ok, agent_token_id} error -> error end rescue Ecto.NoResultsError -> {:error, :agent_not_found} end defp handle_global_default_success(socket, agent_token_id) do message = if agent_token_id, do: t_equipment("Global default cloud poller set successfully"), else: t_equipment("Global default cloud poller cleared") {:noreply, socket |> assign(:global_default_cloud_poller_id, agent_token_id) |> assign(:selected_global_default, agent_token_id || "") |> put_flash(:info, message)} end end