From 88323a229067b42a18ae50bc6436991596adf57a Mon Sep 17 00:00:00 2001 From: Graham McIntie Date: Fri, 13 Mar 2026 16:16:27 -0500 Subject: [PATCH] Deduplicate integrations: settings tab links to standalone page - Replace inline integrations tab with link to /settings/integrations - Remove ~1500 lines of duplicated integration UI from settings - Remove duplicated integration event handlers from settings_live - Standalone IntegrationsLive page remains the single source of truth - Settings page down from ~2400 to ~900 lines --- lib/towerops_web/live/org/settings_live.ex | 133 -- .../live/org/settings_live.html.heex | 1416 +---------------- 2 files changed, 20 insertions(+), 1529 deletions(-) diff --git a/lib/towerops_web/live/org/settings_live.ex b/lib/towerops_web/live/org/settings_live.ex index 0ce813e9..e427822b 100644 --- a/lib/towerops_web/live/org/settings_live.ex +++ b/lib/towerops_web/live/org/settings_live.ex @@ -20,123 +20,6 @@ defmodule ToweropsWeb.Org.SettingsLive do @billing_provider_ids ~w(gaiia sonar splynx visp) - @providers [ - # Billing providers (mutually exclusive — grouped first) - %{ - id: "gaiia", - name: "Gaiia", - description: - "Billing and subscriber management. Enables outage impact analysis, inventory reconciliation, and subscriber-aware monitoring.", - icon: "hero-user-group", - setup_help: %{ - url_pattern: "https://app.gaiia.com/{your-org}/admin/apiKeys", - steps: [ - "Log into Gaiia and go to **Admin** → **API Keys**", - "Click **Create API Key**", - "Give it a descriptive name (e.g. \"TowerOps\")", - "Copy the generated API key and paste it below" - ], - permissions: - "Read-only access. The key only needs permission to read accounts, subscriptions, and inventory data." - } - }, - %{ - id: "sonar", - name: "Sonar", - description: - "Billing and subscriber management. Sync accounts, network sites, and inventory from your Sonar instance.", - icon: "hero-currency-dollar", - setup_help: %{ - url_pattern: "https://{your-instance}.sonar.software/admin/users/{your-user-id}", - steps: [ - "Log into Sonar and go to **Settings** → **Security** → **API Tokens** (or your user profile → API Tokens)", - "Click **Create Token**", - "Name it (e.g. \"TowerOps\") and copy the generated token" - ], - permissions: - "Requires a **read-only** personal access token. The token needs access to: Accounts, Network Sites, Inventory Items, and Address data." - } - }, - %{ - id: "splynx", - name: "Splynx", - description: - "ISP billing and management platform. Sync customers, internet services, and network routers from Splynx.", - icon: "hero-banknotes", - setup_help: %{ - url_pattern: "https://{your-instance}.splynx.app/admin/administration/api-keys", - steps: [ - "Log into Splynx and go to **Administration** → **API Keys**", - "Click **Add** to create a new API key", - "Copy both the **API Key** and **API Secret**" - ], - permissions: - "Requires read access to **Customers**, **Internet Services**, and **Routers**. No write permissions needed." - } - }, - %{ - id: "visp", - name: "VISP", - description: "Cloud-based ISP management. Sync subscribers, sites, equipment, and MRR data from VISP.", - icon: "hero-cloud", - setup_help: %{ - url_pattern: "https://app.visp.net/settings/api", - steps: [ - "Log into VISP and go to **Settings** → **API**", - "Click **Generate New Key**", - "Name it (e.g. \"TowerOps\") and copy the generated API key" - ], - permissions: "Requires read-only API access to **Subscribers**, **Sites**, **Equipment**, and **Billing** data." - } - }, - # Quality of Experience - %{ - id: "preseem", - name: "Preseem", - description: "QoE monitoring and subscriber experience analytics for wireless ISPs.", - icon: "hero-signal", - category: "Quality of Experience", - setup_help: %{ - url_pattern: "https://app.preseem.com/settings/api", - steps: [ - "Log into Preseem and go to **Settings** → **API**", - "Click **Generate API Key**", - "Copy the generated key and paste it below" - ], - permissions: "Read-only access. The key needs permission to read subscriber QoE metrics and access point data." - } - }, - # Incident Management - %{ - id: "pagerduty", - name: "PagerDuty", - description: - "Incident management and on-call alerting. Automatically triggers, acknowledges, and resolves PagerDuty incidents from TowerOps alerts.", - icon: "hero-bell-alert", - category: "Incident Management" - }, - # Infrastructure & IPAM - %{ - id: "netbox", - name: "NetBox", - description: - "Infrastructure source of truth. Sync devices, sites, IP addresses, and interfaces between TowerOps and your NetBox instance.", - icon: "hero-server-stack", - category: "Infrastructure & IPAM", - setup_help: %{ - url_pattern: "https://{your-netbox-instance}/user/api-tokens/", - steps: [ - "Log into NetBox and go to your **profile** (top right) → **API Tokens**", - "Click **Add a token**", - "Optionally set an expiry date and description (e.g. \"TowerOps\")", - "Copy the generated token" - ], - permissions: - "Requires **read** permission at minimum. If you want to push data to NetBox (bidirectional sync), the token also needs **write** permission." - } - } - ] - @impl true def mount(_params, _session, socket) do organization = socket.assigns.current_scope.organization @@ -173,15 +56,7 @@ defmodule ToweropsWeb.Org.SettingsLive do |> assign(:pending_invitations, []) |> assign(:invite_form, to_form(%{"email" => "", "role" => "technician"})) # Integration assigns - loaded lazily when tab is selected - |> assign(:providers, @providers) - |> assign(:billing_provider_ids, @billing_provider_ids) |> assign(:integrations, %{}) - |> assign(:active_billing_provider, nil) - |> assign(:configuring, nil) - |> assign(:integration_form, nil) - |> assign(:test_result, nil) - |> assign(:netbox_config, default_netbox_config()) - |> assign(:integration_credentials, %{}) |> assign(:timezone, socket.assigns.current_scope.timezone)} end @@ -210,14 +85,6 @@ defmodule ToweropsWeb.Org.SettingsLive do |> assign(:pending_invitations, Organizations.list_pending_invitations(org_id)) end - defp maybe_load_integrations(socket, "integrations") do - integrations = load_integrations(socket.assigns.organization.id) - - socket - |> assign(:integrations, integrations) - |> assign(:active_billing_provider, active_billing_provider(integrations)) - end - defp maybe_load_integrations(socket, _tab), do: socket # === Org settings events === diff --git a/lib/towerops_web/live/org/settings_live.html.heex b/lib/towerops_web/live/org/settings_live.html.heex index 6ff76986..78169e2e 100644 --- a/lib/towerops_web/live/org/settings_live.html.heex +++ b/lib/towerops_web/live/org/settings_live.html.heex @@ -77,12 +77,8 @@
  • <.link - patch={~p"/orgs/#{@organization.slug}/settings?tab=integrations"} - class={ - if @active_tab == "integrations", - do: "text-indigo-600 dark:text-indigo-400", - else: "" - } + navigate={~p"/orgs/#{@organization.slug}/settings/integrations"} + class="" > {t("Integrations")} @@ -993,1396 +989,24 @@ <% end %> <%= if @active_tab == "integrations" do %> -
    - <%= for {provider, idx} <- Enum.with_index(@providers) do %> - <% billing_locked? = - @active_billing_provider != nil and provider.id in @billing_provider_ids and - provider.id != @active_billing_provider %> - <%!-- Skip locked billing providers — they're shown in the collapsed group below --%> - <%= unless billing_locked? do %> - <%!-- Section headers --%> - <%= if idx == 0 do %> -
    -

    - Billing & Subscriber Management -

    - <%= if @active_billing_provider == nil do %> - - <.icon name="hero-arrow-path" class="h-3 w-3" /> Choose one - - - — only one billing platform can be active at a time - - <% end %> -
    - <% end %> - <%= if Map.has_key?(provider, :category) and (idx == 0 or Enum.at(@providers, idx - 1) |> Map.get(:category) != provider.category) do %> -
    -

    - {provider.category} -

    -
    - <% end %> -
    - "border-red-200 dark:border-red-500/30" - - @integrations[provider.id] && @integrations[provider.id].enabled -> - "border-green-200 dark:border-green-500/20" - - true -> - "border-gray-200 dark:border-white/10" - end - ]}> - <%!-- Left status accent bar --%> -
    - "bg-red-500" - - @integrations[provider.id] && @integrations[provider.id].enabled -> - "bg-green-500" - - @integrations[provider.id] -> - "bg-gray-300 dark:bg-gray-600" - - true -> - "bg-gray-200 dark:bg-gray-700" - end - ]} /> - -
    - <%!-- === Header Row === --%> -
    -
    -
    "bg-blue-100 dark:bg-blue-900/40" - "gaiia" -> "bg-purple-100 dark:bg-purple-900/40" - "pagerduty" -> "bg-emerald-100 dark:bg-emerald-900/40" - "netbox" -> "bg-orange-100 dark:bg-orange-900/40" - "sonar" -> "bg-violet-100 dark:bg-violet-900/40" - "splynx" -> "bg-blue-100 dark:bg-blue-900/40" - "visp" -> "bg-green-100 dark:bg-green-900/40" - _ -> "bg-indigo-100 dark:bg-indigo-900/40" - end - ]}> - <.icon - name={provider.icon} - class={[ - "h-5 w-5", - case provider.id do - "preseem" -> "text-blue-600 dark:text-blue-400" - "gaiia" -> "text-purple-600 dark:text-purple-400" - "pagerduty" -> "text-emerald-600 dark:text-emerald-400" - "netbox" -> "text-orange-600 dark:text-orange-400" - "sonar" -> "text-violet-600 dark:text-violet-400" - "splynx" -> "text-blue-600 dark:text-blue-400" - "visp" -> "text-green-600 dark:text-green-400" - _ -> "text-indigo-600 dark:text-indigo-400" - end - ]} - /> -
    -
    -

    - {provider.name} -

    -

    - {provider.description} -

    -
    -
    - - <%!-- === Actions (top right) === --%> -
    - <%= if integration = @integrations[provider.id] do %> -
    - - {if integration.enabled, do: "Enabled", else: "Disabled"} - - -
    - <% end %> - - -
    -
    - - <%!-- === Status Row (when configured) === --%> - <%= if integration = @integrations[provider.id] do %> - <%!-- Failed sync banner --%> - <%= if integration.last_sync_status == "failed" do %> -
    - <.icon - name="hero-exclamation-triangle" - class="h-5 w-5 shrink-0 text-red-500 dark:text-red-400 mt-0.5" - /> -
    -

    - {t("Last sync failed")} -

    - <%= if integration.last_sync_message && integration.last_sync_message != "" do %> -

    - {integration.last_sync_message} -

    - <% end %> - <%= if integration.last_synced_at do %> -

    - <.timestamp - datetime={integration.last_synced_at} - timezone={@timezone} - format="absolute" - /> -

    - <% end %> -
    -
    - <% else %> -
    - <%!-- Status dot + label --%> -
    - - - {if integration.enabled, do: "Connected", else: "Paused"} - -
    - - <%!-- Last synced --%> - <%= if integration.last_synced_at do %> -
    - <.icon name="hero-clock" class="h-3.5 w-3.5" /> - - {t("Synced")} - <.timestamp - datetime={integration.last_synced_at} - timezone={@timezone} - format="absolute" - /> - -
    - <% else %> - - {t("Never synced")} - - <% end %> - - <%!-- Sync status badge (success/partial) --%> - <%= if integration.last_sync_status && integration.last_sync_status not in ["never", "failed"] do %> - - "bg-green-50 text-green-700 ring-1 ring-inset ring-green-600/20 dark:bg-green-500/10 dark:text-green-400 dark:ring-green-500/20" - - "partial" -> - "bg-yellow-50 text-yellow-700 ring-1 ring-inset ring-yellow-600/20 dark:bg-yellow-500/10 dark:text-yellow-400 dark:ring-yellow-500/20" - - _ -> - "bg-gray-50 text-gray-600 ring-1 ring-inset ring-gray-500/10 dark:bg-gray-500/10 dark:text-gray-400 dark:ring-gray-500/20" - end - ]}> - <%= if integration.last_sync_status == "success" do %> - <.icon name="hero-check-circle-mini" class="h-3.5 w-3.5" /> - <% end %> - {String.capitalize(integration.last_sync_status)} - - <% end %> - - <%!-- Sync message (success/partial) --%> - <%= if integration.last_sync_message && integration.last_sync_message != "" && integration.last_sync_status != "failed" do %> - - {integration.last_sync_message} - - <% end %> - - <%!-- Next sync --%> - <%= if integration.last_synced_at && integration.enabled do %> - - Next in ~{next_sync_minutes( - integration.last_synced_at, - integration.sync_interval_minutes - )}m - - <% end %> - - <%!-- Sync schedule note --%> - <%= if integration.enabled && provider.id == "pagerduty" do %> - - · Event-driven (real-time) - - <% end %> -
    - <% end %> - - <%!-- === Provider Links Row === --%> - <%= if provider.id == "preseem" || provider.id == "gaiia" do %> -
    - <%= if provider.id == "preseem" do %> - <.link - navigate={ - ~p"/orgs/#{@organization.slug}/settings/integrations/preseem/devices" - } - class="inline-flex items-center gap-1.5 rounded-md bg-gray-50 px-3 py-1.5 text-xs font-medium text-gray-700 ring-1 ring-inset ring-gray-200 transition-colors hover:bg-gray-100 dark:bg-white/5 dark:text-gray-300 dark:ring-white/10 dark:hover:bg-white/10" - > - <.icon name="hero-cpu-chip" class="h-3.5 w-3.5" /> Manage Devices - - <.link - navigate={~p"/insights?source=preseem"} - class="inline-flex items-center gap-1.5 rounded-md bg-gray-50 px-3 py-1.5 text-xs font-medium text-gray-700 ring-1 ring-inset ring-gray-200 transition-colors hover:bg-gray-100 dark:bg-white/5 dark:text-gray-300 dark:ring-white/10 dark:hover:bg-white/10" - > - <.icon name="hero-chart-bar" class="h-3.5 w-3.5" /> Network Insights - - <% end %> - <%= if provider.id == "gaiia" do %> - <.link - navigate={ - ~p"/orgs/#{@organization.slug}/settings/integrations/gaiia/mapping" - } - class="inline-flex items-center gap-1.5 rounded-md bg-gray-50 px-3 py-1.5 text-xs font-medium text-gray-700 ring-1 ring-inset ring-gray-200 transition-colors hover:bg-gray-100 dark:bg-white/5 dark:text-gray-300 dark:ring-white/10 dark:hover:bg-white/10" - > - <.icon name="hero-arrows-right-left" class="h-3.5 w-3.5" /> - Device & Site Linking - - <.link - navigate={ - ~p"/orgs/#{@organization.slug}/settings/integrations/gaiia/reconciliation" - } - class="inline-flex items-center gap-1.5 rounded-md bg-gray-50 px-3 py-1.5 text-xs font-medium text-gray-700 ring-1 ring-inset ring-gray-200 transition-colors hover:bg-gray-100 dark:bg-white/5 dark:text-gray-300 dark:ring-white/10 dark:hover:bg-white/10" - > - <.icon name="hero-scale" class="h-3.5 w-3.5" /> Comparison Report - - <% end %> -
    - <% end %> - <% end %> -
    - - <%!-- === Configure Panel === --%> - <%= if @configuring == provider.id do %> -
    - <%= if provider.id == "netbox" do %> - <.form - for={@integration_form} - id="netbox-form" - phx-change="validate_integration" - phx-submit="save_integration" - > -
    - <%!-- Connection Section --%> -
    -
    - <.icon name="hero-link" class="h-4 w-4 text-gray-400" /> -

    - {t("Connection")} -

    -
    -

    - {t("Your NetBox instance URL and API token.")} -

    -
    -
    - - -
    -
    - - -
    -
    -
    -

    - <.icon name="hero-information-circle" class="h-4 w-4" /> - {t("How to create your API token")} -

    -
      - <%= for step <- provider.setup_help.steps do %> -
    1. {raw(format_help_step(step))}
    2. - <% end %> -
    -

    - <.icon - name="hero-shield-check" - class="h-3.5 w-3.5 inline -mt-0.5" - /> - {raw(format_help_step(provider.setup_help.permissions))} -

    -

    - <.icon - name="hero-arrow-top-right-on-square" - class="h-3 w-3 inline -mt-0.5" - /> Example URL: - {provider.setup_help.url_pattern} -

    -
    -
    - - <%!-- Sync Direction Section --%> -
    -
    - <.icon name="hero-arrows-right-left" class="h-4 w-4 text-gray-400" /> -

    - {t("Sync Direction")} -

    -
    -

    - {t( - "Choose how data flows between TowerOps and NetBox. You can change this later." - )} -

    - -
    -
    -
    - - <.icon - name="hero-arrow-down-tray" - class="h-4 w-4 text-indigo-600" - /> NetBox → TowerOps - - - {t( - "NetBox is the source of truth. Import devices, sites, and IPs from NetBox into TowerOps." - )} - -
    -
    -
    -
    - - <.icon name="hero-arrow-up-tray" class="h-4 w-4 text-orange-600" /> - TowerOps → NetBox - - - {t( - "TowerOps is the source of truth. Push discovered devices and monitoring data to NetBox." - )} - -
    -
    -
    -
    - - <.icon - name="hero-arrows-right-left" - class="h-4 w-4 text-green-600" - /> Bidirectional - - - {t( - "Merge data from both systems. Conflicts resolved by most-recently-updated. Requires write token." - )} - -
    -
    -
    -
    - - <%!-- What to Sync Section --%> -
    -
    - <.icon name="hero-circle-stack" class="h-4 w-4 text-gray-400" /> -

    - {t("What to Sync")} -

    -
    -

    - {t( - "Choose which NetBox objects participate in sync. Devices and sites are recommended at minimum." - )} -

    -
    - - - - -
    -
    - - <%!-- Filtering Section --%> -
    -
    - <.icon name="hero-funnel" class="h-4 w-4 text-gray-400" /> -

    - {t("Filters")} -

    - - (optional) - -
    -

    - {t( - "Narrow the sync scope. Leave blank to sync everything. Comma-separated for multiple values." - )} -

    -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    -
    - - <%!-- Sync Interval --%> -
    -
    - <.input - field={@integration_form[:sync_interval_minutes]} - type="number" - label={t("Sync interval (minutes)")} - min="5" - value={ - if(@integrations["netbox"], - do: @integrations["netbox"].sync_interval_minutes, - else: 30 - ) - } - /> -

    - {t( - "How often TowerOps checks NetBox for changes. 30 minutes is recommended for most setups." - )} -

    -
    -
    - - <%!-- Test & Save --%> - <%= if @test_result do %> -
    "bg-green-50 dark:bg-green-900/20" - {:error, _} -> "bg-red-50 dark:bg-red-900/20" - end - ]}> - <%= case @test_result do %> - <% {:ok, msg} -> %> - <.icon - name="hero-check-circle" - class="h-5 w-5 shrink-0 text-green-500 dark:text-green-400" - /> -

    - {msg} -

    - <% {:error, msg} -> %> - <.icon - name="hero-x-circle" - class="h-5 w-5 shrink-0 text-red-500 dark:text-red-400" - /> -

    - {msg} -

    - <% end %> -
    - <% end %> - -
    - - -
    -
    - - <% else %> - <%= if provider.id == "sonar" do %> - <.form - for={@integration_form} - id="sonar-form" - phx-change="validate_integration" - phx-submit="save_integration" - > -
    -
    -
    - <.icon name="hero-link" class="h-4 w-4 text-gray-400" /> -

    - {t("Connection")} -

    -
    -

    - {t("Your Sonar instance URL and API token.")} -

    -
    -
    - - -
    -
    - - -
    -
    -
    -

    - <.icon name="hero-information-circle" class="h-4 w-4" /> - {t("How to create your API token")} -

    -
      - <%= for step <- provider.setup_help.steps do %> -
    1. {raw(format_help_step(step))}
    2. - <% end %> -
    -

    - <.icon - name="hero-shield-check" - class="h-3.5 w-3.5 inline -mt-0.5" - /> - {raw(format_help_step(provider.setup_help.permissions))} -

    -

    - <.icon - name="hero-arrow-top-right-on-square" - class="h-3 w-3 inline -mt-0.5" - /> Example URL: - {provider.setup_help.url_pattern} -

    -
    -
    - -
    -
    - <.input - field={@integration_form[:sync_interval_minutes]} - type="number" - label={t("Sync interval (minutes)")} - min="5" - value={ - if(@integrations["sonar"], - do: @integrations["sonar"].sync_interval_minutes, - else: 10 - ) - } - /> -
    -
    - - <%= if @test_result do %> -
    "bg-green-50 dark:bg-green-900/20" - {:error, _} -> "bg-red-50 dark:bg-red-900/20" - end - ]}> -

    "text-green-800 dark:text-green-300" - {:error, _} -> "text-red-800 dark:text-red-300" - end - ]}> - {elem(@test_result, 1)} -

    -
    - <% end %> - -
    - - -
    -
    - - <% else %> - <%= if provider.id == "splynx" do %> - <.form - for={@integration_form} - id="splynx-form" - phx-change="validate_integration" - phx-submit="save_integration" - > -
    -
    -
    - <.icon name="hero-link" class="h-4 w-4 text-gray-400" /> -

    - {t("Connection")} -

    -
    -

    - {t("Your Splynx instance URL, API key, and API secret.")} -

    -
    -
    - - -
    -
    -
    - - -
    -
    - - -
    -
    -
    -
    -

    - <.icon name="hero-information-circle" class="h-4 w-4" /> - {t("How to create your API key")} -

    -
      - <%= for step <- provider.setup_help.steps do %> -
    1. {raw(format_help_step(step))}
    2. - <% end %> -
    -

    - <.icon - name="hero-shield-check" - class="h-3.5 w-3.5 inline -mt-0.5" - /> - {raw(format_help_step(provider.setup_help.permissions))} -

    -

    - <.icon - name="hero-arrow-top-right-on-square" - class="h-3 w-3 inline -mt-0.5" - /> Example URL: - {provider.setup_help.url_pattern} -

    -
    -
    - -
    -
    - <.input - field={@integration_form[:sync_interval_minutes]} - type="number" - label={t("Sync interval (minutes)")} - min="5" - value={ - if(@integrations["splynx"], - do: @integrations["splynx"].sync_interval_minutes, - else: 10 - ) - } - /> -
    -
    - - <%= if @test_result do %> -
    "bg-green-50 dark:bg-green-900/20" - {:error, _} -> "bg-red-50 dark:bg-red-900/20" - end - ]}> -

    "text-green-800 dark:text-green-300" - {:error, _} -> "text-red-800 dark:text-red-300" - end - ]}> - {elem(@test_result, 1)} -

    -
    - <% end %> - -
    - - -
    -
    - - <% else %> - <.form - for={@integration_form} - id={"#{provider.id}-form"} - phx-change="validate_integration" - phx-submit="save_integration" - > -
    -
    -
    - <.icon name="hero-key" class="h-4 w-4 text-gray-400" /> -

    - {t("Connection Settings")} -

    -
    - -
    - - -
    -
    - - <%= if Map.has_key?(provider, :setup_help) && provider.id != "pagerduty" do %> -
    -

    - <.icon name="hero-information-circle" class="h-4 w-4" /> - {t("How to create your API key")} -

    -
      - <%= for step <- provider.setup_help.steps do %> -
    1. {raw(format_help_step(step))}
    2. - <% end %> -
    -

    - <.icon - name="hero-shield-check" - class="h-3.5 w-3.5 inline -mt-0.5" - /> - {raw(format_help_step(provider.setup_help.permissions))} -

    -

    - <.icon - name="hero-arrow-top-right-on-square" - class="h-3 w-3 inline -mt-0.5" - /> Example URL: - {provider.setup_help.url_pattern} -

    -
    - <% end %> - - <%= if provider.id == "pagerduty" do %> -
    -

    - <.icon name="hero-information-circle" class="h-4 w-4" /> - {t("Where to find your Integration Key")} -

    -
      -
    1. - In PagerDuty, go to {t("Services")} - → select your service (or create one) -
    2. -
    3. Click the {t("Integrations")} tab
    4. -
    5. - Click {t("Add Integration")} - → select {t("Events API v2")} -
    6. -
    7. - Copy the {t("Integration Key")} - and paste it above -
    8. -
    -

    - {t( - "When connected, TowerOps will automatically trigger, acknowledge, and resolve PagerDuty incidents in sync with your alerts." - )} -

    -
    - -
    -
    - <.icon name="hero-arrow-path" class="h-4 w-4 text-gray-400" /> -

    - {t("Two-Way Sync (Webhooks)")} -

    -
    -

    - {t( - "When someone resolves or acknowledges an incident directly in PagerDuty," - )} - {t("TowerOps will automatically update the corresponding alert.")} -

    -
    - -
    - - {ToweropsWeb.Endpoint.url()}/api/v1/webhooks/pagerduty/{@current_scope.organization.id} - -
    -
    - <.input - name="integration[webhook_secret]" - type="password" - label={t("Webhook Signing Secret")} - value={ - if(@integrations[provider.id], - do: - get_in(@integrations[provider.id].credentials, [ - "webhook_secret" - ]) || - "", - else: "" - ) - } - placeholder={ - t("Optional — paste from PagerDuty webhook extension") - } - /> -
    -
      -
    1. - In PagerDuty, go to {t("Integrations")} - → Generic Webhooks (v3) -
    2. -
    3. - {t("Add a subscription with the Webhook URL above")} -
    4. -
    5. - Select events: incident.resolved - and incident.acknowledged -
    6. -
    7. - Copy the {t("Signing Secret")} - and paste it above -
    8. -
    -
    -
    - <% end %> - - <%= if provider.id != "pagerduty" do %> -
    -
    - <.icon name="hero-clock" class="h-4 w-4 text-gray-400" /> -

    - {t("Sync Settings")} -

    -
    -
    - <.input - field={@integration_form[:sync_interval_minutes]} - type="number" - label={t("Sync interval (minutes)")} - min="5" - value={ - if(@integrations[provider.id], - do: @integrations[provider.id].sync_interval_minutes, - else: if(provider.id == "gaiia", do: 15, else: 10) - ) - } - /> -
    -
    - <% end %> - - <%= if @test_result do %> -
    "bg-green-50 dark:bg-green-900/20" - {:error, _} -> "bg-red-50 dark:bg-red-900/20" - end - ]}> -

    "text-green-800 dark:text-green-300" - {:error, _} -> "text-red-800 dark:text-red-300" - end - ]}> - {elem(@test_result, 1)} -

    -
    - <% end %> - -
    - - -
    - - -
    -
    -
    - - <% end %> - <% end %> - <% end %> - - <%!-- Gaiia Webhook Section --%> - <%= if provider.id == "gaiia" && @integrations["gaiia"] do %> -
    -
    - <.icon name="hero-globe-alt" class="h-4 w-4 text-gray-400" /> -

    - {t("Webhook Configuration")} -

    -
    -

    - {t( - "Receive real-time updates from Gaiia when accounts, subscriptions, or inventory items change." - )} -

    - -
    -
    - -
    - - -
    -
    - -
    - -

    - {t( - "Your Gaiia app URL (e.g. https://app.gaiia.com/your-org). Used to create direct links to inventory items and network sites." - )} -

    -
    - -
    -
    - -
    - -

    - {t( - "Paste the secret key generated by Gaiia when you create the webhook." - )} -

    -
    - -
    -
    - -
    -
    - <.icon name="hero-information-circle" class="h-4 w-4" /> - Setup Instructions -
    -
      -
    1. In your Gaiia admin panel, go to Settings → Webhooks
    2. -
    3. Click "Add Webhook"
    4. -
    5. Paste the Webhook URL above
    6. -
    7. Under Events, select "All Events"
    8. -
    9. Gaiia will generate a secret key — copy it
    10. -
    11. Paste the secret key into the Webhook Secret field above
    12. -
    13. Save the webhook
    14. -
    -

    - {t( - "Once configured, Towerops will receive real-time updates when accounts, subscriptions, or inventory items change in Gaiia." - )} -

    -
    -
    -
    - <% end %> -
    - <% end %> -
    - <% end %> - <%!-- Collapsed group for locked billing providers --%> - <%= if @active_billing_provider != nil and provider.id in @billing_provider_ids and provider.id == @active_billing_provider do %> - <% locked_providers = - Enum.filter(@providers, fn p -> - p.id in @billing_provider_ids and p.id != @active_billing_provider - end) %> - <%= if locked_providers != [] do %> -
    - - <.icon - name="hero-chevron-right" - class="h-3.5 w-3.5 transition-transform group-open:rotate-90" - /> - {length(locked_providers)} other billing {ngettext( - "provider", - "providers", - length(locked_providers) - )} available - -
    - <%= for lp <- locked_providers do %> -
    - <.icon name={lp.icon} class="h-4 w-4" /> - {lp.name} - — {lp.description} -
    - <% end %> -

    - Disable {Enum.find_value(@providers, fn p -> - if p.id == @active_billing_provider, do: p.name - end)} to switch to a different billing provider. -

    -
    -
    - <% end %> - <% end %> - <% end %> +
    +
    + <.icon name="hero-arrow-top-right-on-square" class="mx-auto h-10 w-10 text-indigo-400" /> +

    + {t("Integrations have moved to their own page")} +

    +

    + {t("Manage all your integrations from the dedicated integrations page.")} +

    +
    + <.link + navigate={~p"/orgs/#{@organization.slug}/settings/integrations"} + class="rounded-md bg-indigo-600 px-4 py-2.5 text-sm font-semibold text-white shadow-xs hover:bg-indigo-500 dark:bg-indigo-500 dark:hover:bg-indigo-400" + > + {t("Go to Integrations")} + +
    +
    <% end %>