diff --git a/lib/towerops_web/live/org/integrations_live.ex b/lib/towerops_web/live/org/integrations_live.ex
new file mode 100644
index 00000000..3a6346ad
--- /dev/null
+++ b/lib/towerops_web/live/org/integrations_live.ex
@@ -0,0 +1,200 @@
+defmodule ToweropsWeb.Org.IntegrationsLive do
+ @moduledoc false
+ use ToweropsWeb, :live_view
+
+ alias Towerops.Integrations
+ alias Towerops.Integrations.Integration
+ alias Towerops.Preseem.Client, as: PreseemClient
+
+ @providers [
+ %{
+ id: "preseem",
+ name: "Preseem",
+ description: "QoE monitoring and subscriber experience analytics for wireless ISPs.",
+ icon: "hero-signal"
+ }
+ ]
+
+ @impl true
+ def mount(_params, _session, socket) do
+ organization = socket.assigns.current_scope.organization
+ integrations = load_integrations(organization.id)
+
+ {:ok,
+ socket
+ |> assign(:organization, organization)
+ |> assign(:providers, @providers)
+ |> assign(:integrations, integrations)
+ |> assign(:configuring, nil)
+ |> assign(:form, nil)
+ |> assign(:test_result, nil)}
+ end
+
+ @impl true
+ def handle_event("configure", %{"provider" => provider}, socket) do
+ integration = Map.get(socket.assigns.integrations, provider)
+
+ form =
+ case integration do
+ nil ->
+ %Integration{}
+ |> Integrations.change_integration(%{provider: provider})
+ |> to_form()
+
+ existing ->
+ existing
+ |> Integrations.change_integration(%{})
+ |> to_form()
+ end
+
+ {:noreply,
+ socket
+ |> assign(:configuring, provider)
+ |> assign(:form, form)
+ |> assign(:test_result, nil)}
+ end
+
+ @impl true
+ def handle_event("close_config", _params, socket) do
+ {:noreply,
+ socket
+ |> assign(:configuring, nil)
+ |> assign(:form, nil)
+ |> assign(:test_result, nil)}
+ end
+
+ @impl true
+ def handle_event("validate", %{"integration" => params}, socket) do
+ integration = get_current_integration(socket)
+
+ changeset =
+ integration
+ |> Integrations.change_integration(normalize_params(params))
+ |> Map.put(:action, :validate)
+
+ {:noreply, assign(socket, :form, to_form(changeset))}
+ end
+
+ @impl true
+ def handle_event("save", %{"integration" => params}, socket) do
+ organization = socket.assigns.organization
+ provider = socket.assigns.configuring
+ existing = Map.get(socket.assigns.integrations, provider)
+ attrs = normalize_params(params)
+
+ result =
+ case existing do
+ nil ->
+ Integrations.create_integration(organization.id, Map.put(attrs, :provider, provider))
+
+ integration ->
+ Integrations.update_integration(integration, attrs)
+ end
+
+ case result do
+ {:ok, _integration} ->
+ integrations = load_integrations(organization.id)
+
+ {:noreply,
+ socket
+ |> assign(:integrations, integrations)
+ |> assign(:configuring, nil)
+ |> assign(:form, nil)
+ |> assign(:test_result, nil)
+ |> put_flash(:info, "Integration saved successfully")}
+
+ {:error, changeset} ->
+ {:noreply, assign(socket, :form, to_form(changeset))}
+ end
+ end
+
+ @impl true
+ def handle_event("test_connection", _params, socket) do
+ api_key = extract_api_key(socket)
+
+ if api_key == "" or is_nil(api_key) do
+ {:noreply, assign(socket, :test_result, {:error, "Please enter an API key first"})}
+ else
+ case PreseemClient.test_connection(api_key) do
+ {:ok, _body} ->
+ {:noreply, assign(socket, :test_result, {:ok, "Connection successful"})}
+
+ {:error, :unauthorized} ->
+ {:noreply, assign(socket, :test_result, {:error, "Invalid API key"})}
+
+ {:error, :forbidden} ->
+ {:noreply, assign(socket, :test_result, {:error, "Access forbidden"})}
+
+ {:error, reason} ->
+ {:noreply, assign(socket, :test_result, {:error, "Connection failed: #{inspect(reason)}"})}
+ end
+ end
+ end
+
+ @impl true
+ def handle_event("toggle_enabled", %{"provider" => provider}, socket) do
+ case Map.get(socket.assigns.integrations, provider) do
+ nil ->
+ {:noreply, socket}
+
+ integration ->
+ case Integrations.update_integration(integration, %{enabled: !integration.enabled}) do
+ {:ok, _updated} ->
+ integrations = load_integrations(socket.assigns.organization.id)
+
+ {:noreply,
+ socket
+ |> assign(:integrations, integrations)
+ |> put_flash(:info, "Integration updated")}
+
+ {:error, _changeset} ->
+ {:noreply, put_flash(socket, :error, "Failed to update integration")}
+ end
+ end
+ end
+
+ defp load_integrations(organization_id) do
+ organization_id
+ |> Integrations.list_integrations()
+ |> Map.new(fn integration -> {integration.provider, integration} end)
+ end
+
+ defp get_current_integration(socket) do
+ provider = socket.assigns.configuring
+
+ case Map.get(socket.assigns.integrations, provider) do
+ nil -> %Integration{}
+ integration -> integration
+ end
+ end
+
+ defp normalize_params(params) do
+ api_key = Map.get(params, "api_key", "")
+ base_url = Map.get(params, "base_url", "")
+
+ credentials =
+ then(%{"api_key" => api_key}, fn creds ->
+ if base_url == "", do: creds, else: Map.put(creds, "base_url", base_url)
+ end)
+
+ %{credentials: credentials}
+ end
+
+ defp get_credential(nil, _key), do: ""
+
+ defp get_credential(%Integration{credentials: credentials}, key) when is_map(credentials) do
+ Map.get(credentials, key, "")
+ end
+
+ defp get_credential(_integration, _key), do: ""
+
+ defp extract_api_key(socket) do
+ changeset = socket.assigns.form.source
+ data = Ecto.Changeset.apply_changes(changeset)
+
+ case data.credentials do
+ %{"api_key" => key} -> key
+ _ -> nil
+ end
+ end
+end
diff --git a/lib/towerops_web/live/org/integrations_live.html.heex b/lib/towerops_web/live/org/integrations_live.html.heex
new file mode 100644
index 00000000..765a03a8
--- /dev/null
+++ b/lib/towerops_web/live/org/integrations_live.html.heex
@@ -0,0 +1,199 @@
+
+ Connect third-party services to enhance your monitoring capabilities.
+
+ {provider.description}
+ "text-green-800 dark:text-green-200"
+ {:error, _} -> "text-red-800 dark:text-red-200"
+ end
+ ]}>
+ {elem(@test_result, 1)}
+
+ Integrations
+
+
+ {provider.name}
+
+
Manage organization defaults for SNMP, agents, and MikroTik API configuration
+