diff --git a/lib/towerops_web/live/org/settings_live.html.heex b/lib/towerops_web/live/org/settings_live.html.heex index accfa4d3..c5866912 100644 --- a/lib/towerops_web/live/org/settings_live.html.heex +++ b/lib/towerops_web/live/org/settings_live.html.heex @@ -804,214 +804,288 @@ <% end %> <%= if @active_tab == "integrations" do %> -
+
-
-
-
- <.icon name={provider.icon} class="h-6 w-6 text-indigo-600 dark:text-indigo-400" /> -
-
-

- {provider.name} -

-

- {provider.description} -

+ class={[ + "relative overflow-hidden rounded-xl border bg-white shadow-sm transition-all dark:bg-white/[0.03]", + cond do + @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 --%> +
+ "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" + _ -> "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" + _ -> "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" + /> +
+

+ Last sync failed +

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

+ {integration.last_sync_message} +

+ <% end %> <%= if integration.last_synced_at do %> - - Last synced: +

+ <.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" /> + + Synced <.timestamp datetime={integration.last_synced_at} timezone={@timezone} format="absolute" /> - <% end %> +
+ <% else %> + + Never synced + + <% 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" + <%!-- 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-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400" + "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" - "failed" -> - "bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400" + _ -> + "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 %> - _ -> - "bg-gray-100 text-gray-600 dark:bg-gray-800 dark:text-gray-400" - 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 %> - <%= if integration.last_sync_message && integration.last_sync_message != "" 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 %> - <%= 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 provider.id == "gaiia" do %> - <.link - 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" - } - 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 %> - <%= if provider.id == "pagerduty" do %> - Event-driven — no polling needed. TowerOps sends events to PagerDuty in real time: -

    -
  • - - Device down - - → PagerDuty incident triggered (critical) -
  • -
  • - - Alert acknowledged - - → PagerDuty incident acknowledged -
  • -
  • - - Device recovers - - → PagerDuty incident auto-resolved -
  • -
- <% end %> - <%= if provider.id == "netbox" do %> - <% direction = get_credential(integration, "sync_direction") %> Syncs every {integration.sync_interval_minutes} minutes. - <%= case direction do %> - <% "push" -> %> - TowerOps → NetBox (TowerOps is source of truth) - <% "both" -> %> - Bidirectional sync (merge from both systems) - <% _ -> %> - NetBox → TowerOps (NetBox is source of truth) - <% end %> - <% end %> -

-
- <% end %> -
- <% end %> -
-
- -
- <%= if integration = @integrations[provider.id] do %> - + <%!-- 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 %>
+ <%!-- === Configure Panel === --%> <%= if @configuring == provider.id do %> -
+
<%= if provider.id == "netbox" do %> <.form for={@integration_form} @@ -1022,10 +1096,13 @@
<%!-- Connection Section --%>
-

- Connection -

-

+

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

+ Connection +

+
+

Your NetBox instance URL and API token. The token needs read permission at minimum. Write permission is required if you want to push data to NetBox.

@@ -1051,15 +1128,18 @@ <%!-- Sync Direction Section --%>
-

- Sync Direction -

-

+

+ <.icon name="hero-arrows-right-left" class="h-4 w-4 text-gray-400" /> +

+ Sync Direction +

+
+

Choose how data flows between TowerOps and NetBox. You can change this later.

@@ -1160,14 +1240,17 @@ <%!-- What to Sync Section --%>
-

- What to Sync -

-

+

+ <.icon name="hero-circle-stack" class="h-4 w-4 text-gray-400" /> +

+ What to Sync +

+
+

Choose which NetBox objects participate in sync. Devices and sites are recommended at minimum.

-
-
-
-