towerops/lib/towerops_web/live/onboarding_live.ex
Graham McIntire de986bddf6
Prevent Oban polling/monitoring job stacking per device
When the Oban queue backs up, the 60-second uniqueness window expires
and duplicate jobs stack up per device. Switch to period: :infinity so
only one poll/monitor job exists per device at any time. Add replace
option to supersede stale scheduled jobs with updated scheduled_at.
Remove :executing from unique states so self-scheduling works while
the current job runs. Set max_attempts: 1 since retrying stale polls
is pointless.

Also fix all credo --strict issues across the codebase.
2026-02-16 16:37:48 -06:00

256 lines
7 KiB
Elixir

defmodule ToweropsWeb.OnboardingLive do
@moduledoc false
use ToweropsWeb, :live_view
alias Towerops.Agents
alias Towerops.Integrations
alias Towerops.Integrations.Integration
alias Towerops.Organizations
alias Towerops.Sites
alias Towerops.Sites.Site
@steps [:snmp, :site, :billing, :agent]
@billing_providers [
%{id: "gaiia", name: "Gaiia", icon: "hero-user-group"},
%{id: "sonar", name: "Sonar", icon: "hero-currency-dollar"},
%{id: "splynx", name: "Splynx", icon: "hero-banknotes"},
%{id: "visp", name: "VISP", icon: "hero-cloud"}
]
@impl true
def mount(_params, _session, socket) do
organization = socket.assigns.current_scope.organization
changeset = Organizations.change_organization(organization)
site_changeset = Sites.change_site(%Site{}, %{})
{:ok,
socket
|> assign(:page_title, t("Setup"))
|> assign(:organization, organization)
|> assign(:step, :snmp)
|> assign(:steps, @steps)
|> assign(:form, to_form(changeset))
|> assign(:site_form, to_form(site_changeset, as: "site"))
|> assign(:billing_providers, @billing_providers)
|> assign(:selected_provider, nil)
|> assign(:integration_form, nil)
|> assign(:agent_token, nil)
|> assign(:agent_token_value, nil)}
end
@impl true
def handle_params(_params, _url, socket) do
{:noreply, socket}
end
# SNMP step events
@impl true
def handle_event("validate_snmp", %{"organization" => params}, socket) do
changeset =
socket.assigns.organization
|> Organizations.change_organization(params)
|> Map.put(:action, :validate)
{:noreply, assign(socket, :form, to_form(changeset))}
end
@impl true
def handle_event("save_snmp", %{"organization" => params}, socket) do
case Organizations.update_organization(socket.assigns.organization, params) do
{:ok, organization} ->
{:noreply,
socket
|> assign(:organization, organization)
|> put_flash(:info, t("SNMP configuration saved"))
|> assign(:step, :site)}
{:error, changeset} ->
{:noreply, assign(socket, :form, to_form(changeset))}
end
end
# Site step events
@impl true
def handle_event("validate_site", %{"site" => params}, socket) do
changeset =
%Site{}
|> Sites.change_site(params)
|> Map.put(:action, :validate)
{:noreply, assign(socket, :site_form, to_form(changeset, as: "site"))}
end
@impl true
def handle_event("save_site", %{"site" => params}, socket) do
org = socket.assigns.organization
attrs = Map.put(params, "organization_id", org.id)
case Sites.create_site(attrs) do
{:ok, _site} ->
# Enable sites on the org if not already
if !org.use_sites do
Organizations.update_organization(org, %{use_sites: true})
end
{:noreply,
socket
|> put_flash(:info, t("Site created"))
|> assign(:step, :billing)}
{:error, changeset} ->
{:noreply, assign(socket, :site_form, to_form(changeset, as: "site"))}
end
end
# Billing step events
@impl true
def handle_event("select_provider", %{"provider" => provider}, socket) do
form =
%Integration{}
|> Integrations.change_integration(%{provider: provider})
|> to_form()
{:noreply,
socket
|> assign(:selected_provider, provider)
|> assign(:integration_form, form)}
end
@impl true
def handle_event("validate_integration", %{"integration" => params}, socket) do
changeset =
%Integration{}
|> Integrations.change_integration(normalize_integration_params(params, socket.assigns.selected_provider))
|> Map.put(:action, :validate)
{:noreply, assign(socket, :integration_form, to_form(changeset))}
end
@impl true
def handle_event("save_integration", %{"integration" => params}, socket) do
org = socket.assigns.organization
provider = socket.assigns.selected_provider
attrs = params |> normalize_integration_params(provider) |> Map.put(:provider, provider) |> Map.put(:enabled, true)
case Integrations.create_integration(org.id, attrs) do
{:ok, _integration} ->
{:noreply,
socket
|> put_flash(:info, t("Integration saved"))
|> assign(:step, :agent)
|> load_agent_token()}
{:error, changeset} ->
{:noreply, assign(socket, :integration_form, to_form(changeset))}
end
end
# Navigation events
@impl true
def handle_event("skip", _params, socket) do
next = next_step(socket.assigns.step)
socket =
if next == :done do
complete_onboarding(socket)
else
socket = assign(socket, :step, next)
if next == :agent, do: load_agent_token(socket), else: socket
end
{:noreply, socket}
end
@impl true
def handle_event("finish", _params, socket) do
{:noreply, complete_onboarding(socket)}
end
@impl true
def handle_event("go_to_step", %{"step" => step}, socket) do
step = String.to_existing_atom(step)
socket =
if step == :agent do
socket |> assign(:step, step) |> load_agent_token()
else
assign(socket, :step, step)
end
{:noreply, socket}
end
# Private helpers
defp next_step(:snmp), do: :site
defp next_step(:site), do: :billing
defp next_step(:billing), do: :agent
defp next_step(:agent), do: :done
defp step_index(:snmp), do: 0
defp step_index(:site), do: 1
defp step_index(:billing), do: 2
defp step_index(:agent), do: 3
defp step_label(:snmp), do: t("SNMP")
defp step_label(:site), do: t("Site")
defp step_label(:billing), do: t("Billing")
defp step_label(:agent), do: t("Agent")
defp complete_onboarding(socket) do
org = socket.assigns.organization
Organizations.update_organization(org, %{onboarding_completed: true})
socket
|> put_flash(:info, t("Setup complete"))
|> push_navigate(to: ~p"/dashboard")
end
defp load_agent_token(socket) do
org = socket.assigns.organization
tokens = Agents.list_organization_agent_tokens(org.id)
case tokens do
[token | _] ->
assign(socket, agent_token: token, agent_token_value: nil)
[] ->
case Agents.create_agent_token(org.id, "Default Agent") do
{:ok, token, token_value} ->
assign(socket, agent_token: token, agent_token_value: token_value)
{:error, _} ->
assign(socket, agent_token: nil, agent_token_value: nil)
end
end
end
defp normalize_integration_params(params, "sonar") do
%{
credentials: %{
"instance_url" => String.trim(Map.get(params, "instance_url", "")),
"api_token" => Map.get(params, "api_token", "")
}
}
end
defp normalize_integration_params(params, "splynx") do
%{
credentials: %{
"instance_url" => String.trim(Map.get(params, "instance_url", "")),
"api_key" => Map.get(params, "api_key", ""),
"api_secret" => Map.get(params, "api_secret", "")
}
}
end
defp normalize_integration_params(params, _provider) do
%{credentials: %{"api_key" => Map.get(params, "api_key", "")}}
end
end