+ <%= if active_billing do %>
+ <%!-- Active billing provider --%>
+ <% integration = @integrations[active_billing.id] %>
+
+
+
+ <.icon name={active_billing.icon} class="h-6 w-6 text-indigo-600 dark:text-indigo-400" />
+
+
+
+
+ {active_billing.name}
+
+
+ Connected
+
+
+ {active_billing.description}
+
+
<%= if integration.last_synced_at do %>
{t("Last synced:")}
@@ -81,17 +71,10 @@
- "bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400"
-
- "partial" ->
- "bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400"
-
- "failed" ->
- "bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400"
-
- _ ->
- "bg-gray-100 text-gray-600 dark:bg-gray-800 dark:text-gray-400"
+ "success" -> "bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400"
+ "partial" -> "bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400"
+ "failed" -> "bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400"
+ _ -> "bg-gray-100 text-gray-600 dark:bg-gray-800 dark:text-gray-400"
end
]}>
{String.capitalize(integration.last_sync_status)}
@@ -100,104 +83,95 @@
<%= if integration.last_synced_at do %>
- · Next sync in ~{next_sync_minutes(
- integration.last_synced_at,
- integration.sync_interval_minutes
- )}m
+ · Next sync in ~{next_sync_minutes(integration.last_synced_at, integration.sync_interval_minutes)}m
<% end %>
- <%= if provider.id == "preseem" do %>
+ <%= if active_billing.id == "gaiia" do %>
<.link
- navigate={
- ~p"/orgs/#{@organization.slug}/settings/integrations/preseem/devices"
- }
- class="text-sm text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"
- >
- Manage Devices →
-
- <.link
- navigate={~p"/insights?source=preseem"}
- class="text-sm text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"
- >
- Network Insights →
-
- <% end %>
- <%= if provider.id == "gaiia" do %>
- <.link
- navigate={
- ~p"/orgs/#{@organization.slug}/settings/integrations/gaiia/mapping"
- }
+ navigate={~p"/orgs/#{@organization.slug}/settings/integrations/gaiia/mapping"}
class="text-sm text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"
>
Entity Mapping →
<.link
- navigate={
- ~p"/orgs/#{@organization.slug}/settings/integrations/gaiia/reconciliation"
- }
+ navigate={~p"/orgs/#{@organization.slug}/settings/integrations/gaiia/reconciliation"}
class="text-sm text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"
>
Reconciliation →
<% end %>
- <%!-- Sync schedule info --%>
- <%= if integration.enabled do %>
-
-
- <.icon name="hero-clock" class="h-3.5 w-3.5 inline" /> Sync Schedule
-
-
- <%= if provider.id == "preseem" do %>
- Syncs every {integration.sync_interval_minutes} minutes. Baselines and fleet profiles computed nightly.
- <% end %>
- <%= if provider.id == "gaiia" do %>
- Syncs every {integration.sync_interval_minutes} minutes. Reconciliation runs nightly. Also receives real-time webhook updates.
- <% end %>
-
-
- <% end %>
- <% end %>
+
-
-
- <%= if integration = @integrations[provider.id] do %>
+
-
-
+
- <% end %>
+
+ {t("Configure")}
+
+
+
+ <%!-- Switch provider link --%>
+
+
+ Using {active_billing.name}. To switch providers, disable this integration first.
+
+
+ <% else %>
+ <%!-- No active billing — show provider picker --%>
+
+ {t("Select your billing platform to get started:")}
+
+
- {t("Configure")}
+
+ <.icon name={provider.icon} class="h-5 w-5 text-indigo-600 dark:text-indigo-400" />
+
+
+ {provider.name}
+
+
+ {provider.description}
+
-
+ <% end %>
- <%= if @configuring == provider.id do %>
+ <%!-- Config form for whichever billing provider is being configured --%>
+ <%= if @configuring && find_category_id(@configuring) == "billing" do %>
+ <% provider = Enum.find(category.providers, & &1.id == @configuring) %>
+
+ Configure {provider.name}
+
<.form
for={@form}
id={"#{provider.id}-form"}
@@ -256,7 +230,6 @@
>
{t("Test Connection")}
-
- <% end %>
- <%= if provider.id == "gaiia" && @integrations["gaiia"] do %>
-
-
- {t("Webhook Configuration")}
-
-
- {t(
- "Receive real-time updates from Gaiia when accounts, subscriptions, or inventory items change."
- )}
-
+ <%!-- Gaiia webhook config --%>
+ <%= if @configuring == "gaiia" && @integrations["gaiia"] do %>
+
+
+ {t("Webhook Configuration")}
+
+
+ {t("Receive real-time updates from Gaiia when accounts, subscriptions, or inventory items change.")}
+
-
-
-
- {t("Webhook URL")}
-
-
-
-
- <.icon name="hero-clipboard" class="h-3.5 w-3.5" /> Copy
-
+
+
+
+ {t("Webhook URL")}
+
+
+
+
+ <.icon name="hero-clipboard" class="h-3.5 w-3.5" /> Copy
+
+
-
-
-
- {t("Webhook Secret")}
-
-
-
-
- <.icon name="hero-clipboard" class="h-3.5 w-3.5" /> Copy
-
+
+
+ {t("Webhook Secret")}
+
+
+
+
+ <.icon name="hero-clipboard" class="h-3.5 w-3.5" /> Copy
+
+
+
+
+ {t("Regenerate Secret")}
+
+
-
-
- {t("Regenerate Secret")}
-
-
-
-
-
- {t("Setup Instructions")}
-
-
- In your Gaiia admin panel, go to Settings → Webhooks
- Click "Add Webhook"
- Paste the Webhook URL above
- Paste the Webhook Secret above into the "Secret" field
- Select events: Account, Billing Subscription, Inventory Item
- Save the webhook
-
-
- {t(
- "Once configured, Towerops will receive real-time updates when accounts, subscriptions, or inventory items change in Gaiia."
- )}
-
+
+
+ {t("Setup Instructions")}
+
+
+ In your Gaiia admin panel, go to Settings → Webhooks
+ Click "Add Webhook"
+ Paste the Webhook URL above
+ Paste the Webhook Secret above into the "Secret" field
+ Select events: Account, Billing Subscription, Inventory Item
+ Save the webhook
+
+
-
+ <% end %>
<% end %>
-
+ <% else %>
+ <%!-- Non-exclusive categories: one card per provider --%>
+
+
+
+
+
+ <.icon name={provider.icon} class="h-6 w-6 text-indigo-600 dark:text-indigo-400" />
+
+
+
+ {provider.name}
+
+
+ {provider.description}
+
+
+ <%= if integration = @integrations[provider.id] do %>
+
+
+ {if integration.enabled, do: "Enabled", else: "Disabled"}
+
+
+ <%= if integration.last_synced_at do %>
+
+ {t("Last synced:")}
+ <.timestamp
+ datetime={integration.last_synced_at}
+ timezone={@timezone}
+ format="absolute"
+ />
+
+ <% end %>
+
+ <%= if integration.last_sync_status && integration.last_sync_status != "never" do %>
+
"bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400"
+ "partial" -> "bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400"
+ "failed" -> "bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400"
+ _ -> "bg-gray-100 text-gray-600 dark:bg-gray-800 dark:text-gray-400"
+ end
+ ]}>
+ {String.capitalize(integration.last_sync_status)}
+
+ <% end %>
+
+ <%= if integration.last_synced_at do %>
+
+ · Next sync in ~{next_sync_minutes(integration.last_synced_at, integration.sync_interval_minutes)}m
+
+ <% end %>
+
+ <%= if provider.id == "preseem" do %>
+ <.link
+ navigate={~p"/orgs/#{@organization.slug}/settings/integrations/preseem/devices"}
+ class="text-sm text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"
+ >
+ Manage Devices →
+
+ <.link
+ navigate={~p"/insights?source=preseem"}
+ class="text-sm text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"
+ >
+ Network Insights →
+
+ <% end %>
+
+ <%= if integration.enabled do %>
+
+
+ <.icon name="hero-clock" class="h-3.5 w-3.5 inline" /> Sync Schedule
+
+
+ <%= if provider.id == "preseem" do %>
+ Syncs every {integration.sync_interval_minutes} minutes. Baselines and fleet profiles computed nightly.
+ <% end %>
+
+
+ <% end %>
+
+ <% end %>
+
+
+
+
+ <%= if integration = @integrations[provider.id] do %>
+
+
+
+ <% end %>
+
+
+ {t("Configure")}
+
+
+
+
+ <%= if @configuring == provider.id do %>
+
+ <.form
+ for={@form}
+ id={"#{provider.id}-form"}
+ phx-change="validate"
+ phx-submit="save"
+ >
+
+ <.input
+ field={@form[:api_key]}
+ type="password"
+ label={t("API Key")}
+ placeholder={"Enter your #{provider.name} API key"}
+ autocomplete="off"
+ value={get_credential(@integrations[provider.id], "api_key")}
+ />
+
+ <.input
+ field={@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: 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-200"
+ {:error, _} -> "text-red-800 dark:text-red-200"
+ end
+ ]}>
+ {elem(@test_result, 1)}
+
+
+ <% end %>
+
+
+
+ {t("Test Connection")}
+
+
+
+ {t("Cancel")}
+
+
+ {t("Save")}
+
+
+
+
+
+
+ <% end %>
+
+
+ <% end %>
diff --git a/test/towerops_web/live/org/integrations_live_test.exs b/test/towerops_web/live/org/integrations_live_test.exs
index fd9100c6..3b0d180d 100644
--- a/test/towerops_web/live/org/integrations_live_test.exs
+++ b/test/towerops_web/live/org/integrations_live_test.exs
@@ -65,7 +65,7 @@ defmodule ToweropsWeb.Org.IntegrationsLiveTest do
assert integration.credentials["webhook_secret"] == "existing-secret-abc"
end
- test "displays webhook URL when gaiia integration exists", %{
+ test "displays webhook URL when gaiia integration is configured", %{
conn: conn,
user: user,
organization: org
@@ -77,15 +77,16 @@ defmodule ToweropsWeb.Org.IntegrationsLiveTest do
credentials: %{"api_key" => "key", "webhook_secret" => "secret123"}
})
- {:ok, _view, html} =
+ {:ok, view, _html} =
conn
|> log_in_user(user)
|> live(~p"/orgs/#{org.slug}/settings/integrations")
+ html = view |> element("#configure-gaiia") |> render_click()
assert html =~ "/api/v1/webhooks/gaiia/#{org.id}"
end
- test "displays webhook secret when gaiia integration exists", %{
+ test "displays webhook secret when gaiia integration is configured", %{
conn: conn,
user: user,
organization: org
@@ -97,11 +98,12 @@ defmodule ToweropsWeb.Org.IntegrationsLiveTest do
credentials: %{"api_key" => "key", "webhook_secret" => "secret123"}
})
- {:ok, _view, html} =
+ {:ok, view, _html} =
conn
|> log_in_user(user)
|> live(~p"/orgs/#{org.slug}/settings/integrations")
+ html = view |> element("#configure-gaiia") |> render_click()
assert html =~ "secret123"
end
@@ -122,6 +124,7 @@ defmodule ToweropsWeb.Org.IntegrationsLiveTest do
|> log_in_user(user)
|> live(~p"/orgs/#{org.slug}/settings/integrations")
+ view |> element("#configure-gaiia") |> render_click()
view |> element("#regenerate-webhook-secret") |> render_click()
assert {:ok, integration} = Towerops.Integrations.get_integration(org.id, "gaiia")
@@ -130,7 +133,7 @@ defmodule ToweropsWeb.Org.IntegrationsLiveTest do
assert integration.credentials["api_key"] == "key"
end
- test "shows setup instructions when gaiia integration exists", %{
+ test "shows setup instructions when gaiia is being configured", %{
conn: conn,
user: user,
organization: org
@@ -142,11 +145,12 @@ defmodule ToweropsWeb.Org.IntegrationsLiveTest do
credentials: %{"api_key" => "key", "webhook_secret" => "secret"}
})
- {:ok, _view, html} =
+ {:ok, view, _html} =
conn
|> log_in_user(user)
|> live(~p"/orgs/#{org.slug}/settings/integrations")
+ html = view |> element("#configure-gaiia") |> render_click()
assert html =~ "Webhook Configuration"
assert html =~ "Webhook URL"
assert html =~ "Webhook Secret"