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"} - - + +
+ <% end %> + + +
+ <% end %> +
+ + <%!-- === 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 %> - -
- - - <%!-- === 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