From 41708651a036fec0442ed0b0c10e6a4b146b4c20 Mon Sep 17 00:00:00 2001 From: Graham McIntire Date: Wed, 11 Mar 2026 10:09:27 -0500 Subject: [PATCH] refactor: collapse inactive billing providers into expandable disclosure When a billing provider is active, all other billing providers are hidden from the main list and collected into a compact
disclosure element showing provider names and descriptions. --- .../live/org/settings_live.html.heex | 1725 +++++++++-------- 1 file changed, 878 insertions(+), 847 deletions(-) diff --git a/lib/towerops_web/live/org/settings_live.html.heex b/lib/towerops_web/live/org/settings_live.html.heex index 9b9d88e7..9796f377 100644 --- a/lib/towerops_web/live/org/settings_live.html.heex +++ b/lib/towerops_web/live/org/settings_live.html.heex @@ -868,118 +868,109 @@ <% billing_locked? = @active_billing_provider != nil and provider.id in @billing_provider_ids and provider.id != @active_billing_provider %> - <%!-- Section headers --%> - <%= if idx == 0 do %> -
-

- Billing & Subscriber Management -

- - <.icon name="hero-arrow-path" class="h-3 w-3" /> Choose one - - - — only one billing platform can be active at a time - -
- <% 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-gray-200 dark:border-white/10" - - @integrations[provider.id] && @integrations[provider.id].last_sync_status == "failed" -> - "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 --%> + <%!-- 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 %>
- "bg-red-500" + "border-red-200 dark:border-red-500/30" @integrations[provider.id] && @integrations[provider.id].enabled -> - "bg-green-500" - - @integrations[provider.id] -> - "bg-gray-300 dark:bg-gray-600" + "border-green-200 dark:border-green-500/20" true -> - "bg-gray-200 dark:bg-gray-700" + "border-gray-200 dark:border-white/10" end - ]} /> + ]}> + <%!-- Left status accent bar --%> +
+ "bg-red-500" -
- <%!-- === 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} -

-
-
+ @integrations[provider.id] && @integrations[provider.id].enabled -> + "bg-green-500" - <%!-- === Actions (top right) === --%> - <%= if billing_locked? do %> -
- - <.icon name="hero-lock-closed-mini" class="h-3.5 w-3.5" /> - {Enum.find_value(@providers, fn p -> - if p.id == @active_billing_provider, do: p.name - end)} is active - + @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} +

+
- <% else %> + + <%!-- === Actions (top right) === --%>
<%= if integration = @integrations[provider.id] do %>
@@ -1031,11 +1022,9 @@ {if @configuring == provider.id, do: "Close", else: "Configure"}
- <% end %> -
+
- <%!-- === Status Row (when configured, hidden for locked billing providers) === --%> - <%= if !billing_locked? do %> + <%!-- === Status Row (when configured) === --%> <%= if integration = @integrations[provider.id] do %> <%!-- Failed sync banner --%> <%= if integration.last_sync_status == "failed" do %> @@ -1187,430 +1176,20 @@
<% end %> <% end %> - <% end %> -
+
- <%!-- === Configure Panel === --%> - <%= if !billing_locked? && @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 %> + <%!-- === Configure Panel === --%> + <%= if @configuring == provider.id do %> +
+ <%= if provider.id == "netbox" do %> <.form for={@integration_form} - id="sonar-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" /> @@ -1619,18 +1198,18 @@

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

@@ -1642,8 +1221,8 @@ @@ -1676,7 +1255,273 @@
-
+ <%!-- 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]} @@ -1684,15 +1529,21 @@ label={t("Sync interval (minutes)")} min="5" value={ - if(@integrations["sonar"], - do: @integrations["sonar"].sync_interval_minutes, - else: 10 + 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-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)} -

+ <%= 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 %> @@ -1733,10 +1593,10 @@
<% else %> - <%= if provider.id == "splynx" do %> + <%= if provider.id == "sonar" do %> <.form for={@integration_form} - id="splynx-form" + id="sonar-form" phx-change="validate_integration" phx-submit="save_integration" > @@ -1749,9 +1609,9 @@

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

-
+
-
-
- - -
-
- - -
+
+ +

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

    <%= for step <- provider.setup_help.steps do %> @@ -1829,8 +1674,8 @@ label={t("Sync interval (minutes)")} min="5" value={ - if(@integrations["splynx"], - do: @integrations["splynx"].sync_interval_minutes, + if(@integrations["sonar"], + do: @integrations["sonar"].sync_interval_minutes, else: 10 ) } @@ -1878,169 +1723,95 @@
<% 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 provider.id == "splynx" do %> + <.form + for={@integration_form} + id="splynx-form" + phx-change="validate_integration" + phx-submit="save_integration" + > +
- - -
-
- - <%= 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" /> +
+ <.icon name="hero-link" class="h-4 w-4 text-gray-400" />

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

-

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

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

-
- -
- - {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. +
    +

    + <.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-clock" class="h-4 w-4 text-gray-400" /> -

- {t("Sync Settings")} -

-
<.input field={@integration_form[:sync_interval_minutes]} @@ -2048,54 +1819,43 @@ 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) + if(@integrations["splynx"], + do: @integrations["splynx"].sync_interval_minutes, + 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" + {:ok, _} -> "bg-green-50 dark:bg-green-900/20" + {:error, _} -> "bg-red-50 dark:bg-red-900/20" end ]}> - {elem(@test_result, 1)} -

-
- <% 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 %> - <% 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." - )} -

+ <%!-- 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( + "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." + )} +

- -
- -

- {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 %> +
+ <% 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 %>
<% end %>