diff --git a/lib/towerops_web/live/org/settings_live.ex b/lib/towerops_web/live/org/settings_live.ex
index bb559b33..213cad37 100644
--- a/lib/towerops_web/live/org/settings_live.ex
+++ b/lib/towerops_web/live/org/settings_live.ex
@@ -176,6 +176,7 @@ defmodule ToweropsWeb.Org.SettingsLive do
|> 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)
@@ -205,7 +206,10 @@ defmodule ToweropsWeb.Org.SettingsLive do
defp maybe_load_integrations(socket, "integrations") do
integrations = load_integrations(socket.assigns.organization.id)
- assign(socket, :integrations, integrations)
+
+ socket
+ |> assign(:integrations, integrations)
+ |> assign(:active_billing_provider, active_billing_provider(integrations))
end
defp maybe_load_integrations(socket, _tab), do: socket
@@ -480,6 +484,7 @@ defmodule ToweropsWeb.Org.SettingsLive do
{:noreply,
socket
|> assign(:integrations, integrations)
+ |> assign(:active_billing_provider, active_billing_provider(integrations))
|> assign(:configuring, nil)
|> assign(:integration_form, nil)
|> assign(:test_result, nil)
@@ -513,6 +518,7 @@ defmodule ToweropsWeb.Org.SettingsLive do
{:noreply,
socket
|> assign(:integrations, integrations)
+ |> assign(:active_billing_provider, active_billing_provider(integrations))
|> put_flash(:info, t("Webhook secret saved"))}
{:error, _changeset} ->
@@ -535,6 +541,7 @@ defmodule ToweropsWeb.Org.SettingsLive do
{:noreply,
socket
|> assign(:integrations, integrations)
+ |> assign(:active_billing_provider, active_billing_provider(integrations))
|> put_flash(:info, t("Integration updated"))}
{:error, _changeset} ->
@@ -649,6 +656,15 @@ defmodule ToweropsWeb.Org.SettingsLive do
|> Map.new(fn integration -> {integration.provider, integration} end)
end
+ defp active_billing_provider(integrations) do
+ Enum.find_value(@billing_provider_ids, fn id ->
+ case Map.get(integrations, id) do
+ %{enabled: true} -> id
+ _ -> nil
+ end
+ end)
+ end
+
defp get_current_integration(socket) do
provider = socket.assigns.configuring
diff --git a/lib/towerops_web/live/org/settings_live.html.heex b/lib/towerops_web/live/org/settings_live.html.heex
index 257a643a..9b9d88e7 100644
--- a/lib/towerops_web/live/org/settings_live.html.heex
+++ b/lib/towerops_web/live/org/settings_live.html.heex
@@ -865,6 +865,9 @@
<%= 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 %>
<%!-- Section headers --%>
<%= if idx == 0 do %>
@@ -888,7 +891,11 @@
<% 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"
@@ -963,215 +970,228 @@
<%!-- === Actions (top right) === --%>
-
- <%= if integration = @integrations[provider.id] do %>
-
-
- {if integration.enabled, do: "Enabled", else: "Disabled"}
-
-
+
+ <%!-- === Status Row (when configured, hidden for locked billing providers) === --%>
+ <%= if !billing_locked? do %>
+ <%= 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 %>
-
- {if @configuring == provider.id, do: "Close", else: "Configure"}
-
-
-
-
- <%!-- === 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}
-
+ <%!-- === 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 integration.last_synced_at do %>
-
- <.timestamp
- datetime={integration.last_synced_at}
- timezone={@timezone}
- format="absolute"
- />
-
+ <%= 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" /> Entity Mapping
+
+ <.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" /> Reconciliation
+
<% 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" /> Entity Mapping
-
- <.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" /> Reconciliation
-
- <% end %>
-
+ <% end %>
<% end %>
<% end %>
<%!-- === Configure Panel === --%>
- <%= if @configuring == provider.id do %>
+ <%= if !billing_locked? && @configuring == provider.id do %>
<%= if provider.id == "netbox" do %>
<.form