<.link navigate={~p"/dashboard"} class="inline-flex items-center gap-1 text-sm text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white" > <.icon name="hero-arrow-left" class="h-4 w-4" /> Back to Dashboard

{t("Organization Settings")}

{t("Manage organization defaults, integrations, and configuration")}

<%= if @active_tab not in ["integrations", "members"] do %> <.form for={@form} id="organization-form" phx-change="validate" phx-submit="save" data-1p-ignore data-form-type="other" >
<%= if @active_tab == "general" do %>

{t("Organization Name")}

{t("Update the name of your organization.")}

<.input field={@form[:name]} type="text" label={t("Organization Name")} required />
<%= if @user_orgs_count > 1 do %>

{t("Default Organization")}

{t( "Set this organization as your default. When you log in, you'll be directed to your default organization." )}

<%= if @membership.is_default do %>
<.icon name="hero-check-circle" class="h-5 w-5 text-green-600 dark:text-green-400" />

{t("This is your default organization")}

<% else %>

{t("Click to make this your default organization")}

<% end %>
<% end %>

{t("Site Organization")}

{t( "Enable site organization to group devices into physical locations (offices, datacenters, etc.)." )}

<.input field={@form[:use_sites]} type="checkbox" label={t("Use sites to organize devices")} />

{t( "When enabled, you can organize devices into sites. When disabled, all devices belong directly to the organization." )}

<%= if @organization.use_sites && @form[:use_sites].value == false do %>
<.icon name="hero-exclamation-triangle" class="h-5 w-5 text-amber-400" />

{t("Warning: Disabling Sites")}

{t( "All devices will be removed from their current sites and assigned directly to the organization." )} {t( "Site assignments will be lost, but devices will remain in the organization." )}

<% end %>
<% end %> <%= if @active_tab == "snmp" do %>

{t("SNMP Configuration")}

{t( "Set default SNMP settings for all devices in this organization. These can be overridden at the site or device level." )}

<.icon name="hero-information-circle" class="h-4 w-4 inline" /> Hierarchy: Device > Site > Organization

<.input field={@form[:snmp_version]} type="select" label={t("SNMP Version")} prompt="Select SNMP version" options={[{"v1", "1"}, {"v2c", "2c"}, {"v3", "3"}]} /> <%= if @form[:snmp_version].value in ["1", "2c"] do %> <.input field={@form[:snmp_community]} type="text" label={t("SNMP Community String")} placeholder="e.g., public" /> <% end %> <%= if @form[:snmp_version].value == "3" do %> <.input field={@form[:snmpv3_security_level]} type="select" label={t("Security Level")} prompt="Select security level" options={[ {"No Auth, No Priv", "noAuthNoPriv"}, {"Auth, No Priv", "authNoPriv"}, {"Auth, Priv", "authPriv"} ]} /> <.input field={@form[:snmpv3_username]} type="text" label={t("Username")} placeholder="e.g., snmpuser" /> <%= if @form[:snmpv3_security_level].value in ["authNoPriv", "authPriv"] do %> <.input field={@form[:snmpv3_auth_protocol]} type="select" label={t("Auth Protocol")} prompt="Select protocol" options={[ {"SHA-256 (recommended)", "SHA-256"}, {"SHA-512", "SHA-512"}, {"SHA-384", "SHA-384"}, {"SHA-224", "SHA-224"}, {"SHA (SHA-1)", "SHA"}, {"MD5", "MD5"} ]} /> <.input field={@form[:snmpv3_auth_password]} type="password" label={t("Auth Password")} placeholder={t("Min 8 characters")} autocomplete="off" /> <% end %> <%= if @form[:snmpv3_security_level].value == "authPriv" do %> <.input field={@form[:snmpv3_priv_protocol]} type="select" label={t("Privacy Protocol")} prompt="Select protocol" options={[ {"AES-128 (recommended)", "AES"}, {"AES-256", "AES-256"}, {"AES-192", "AES-192"}, {"AES-256-C", "AES-256-C"}, {"DES (legacy)", "DES"} ]} /> <.input field={@form[:snmpv3_priv_password]} type="password" label={t("Privacy Password")} placeholder={t("Min 8 characters")} autocomplete="off" /> <% end %> <% end %> <.input field={@form[:snmp_port]} type="number" label={t("SNMP Port")} placeholder="161" /> <%= if @organization.snmp_community do %>

{t("Force Apply to All Devices")}

{t( "This will override SNMP settings for ALL devices across all sites in this organization." )}

<% end %>
<% end %> <%= if false && @active_tab == "mikrotik" && @current_scope.user.is_superuser do %>

{t("MikroTik API Configuration")}

{t( "Set default MikroTik RouterOS API credentials for all devices in this organization. Only applies to devices detected as MikroTik." )}

<.icon name="hero-beaker" class="h-4 w-4 inline" /> {t("Experimental Feature:")} {t("MikroTik API integration is under active development.")}

<.icon name="hero-information-circle" class="h-4 w-4 inline" /> Hierarchy: Device > Site > Organization

<.input field={@form[:mikrotik_enabled]} type="checkbox" label={t("Enable MikroTik API")} />
<.input field={@form[:mikrotik_username]} type="text" label={t("Username")} placeholder="e.g., admin" autocomplete="off" />
<.input field={@form[:mikrotik_password]} type="password" label={t("Password")} placeholder={t("RouterOS API password")} autocomplete="off" />
<.input field={@form[:mikrotik_port]} type="number" label={t("API Port")} placeholder="8729" autocomplete="off" />
<.input field={@form[:mikrotik_use_ssl]} type="checkbox" label={t("Use SSL (API-SSL)")} />

{t("Critical Security Warning:")} {t( "Plain API (port 8728) sends credentials unencrypted over the network. This setting is" )} blocked for devices using cloud pollers and should only be used with local agents on trusted networks.

{t("Security Warning:")} {t( "Plain API (port 8728) sends credentials unencrypted. Use SSL (port 8729) whenever possible." )}

<% end %> <%= if @active_tab == "agents" do %> <%= if @available_agents != [] do %>

{t("Default Agent")}

{t( "Select a default agent for SNMP polling. This will be used for all devices in this organization unless overridden at the site or device level." )}

<.icon name="hero-information-circle" class="h-4 w-4 inline" /> Hierarchy: Device > Site > Organization

<.input field={@form[:default_agent_token_id]} type="select" label={t("Default Remote Agent")} prompt="No default agent - cloud polling" options={Enum.map(@available_agents, &{&1.name, &1.id})} />

{t("Current Device Assignment:")}

<.icon name="hero-server" class="h-4 w-4" /> {@assignment_breakdown.direct} device-level override
<.icon name="hero-building-office" class="h-4 w-4" /> {@assignment_breakdown.site} inherited from site
<.icon name="hero-building-office-2" class="h-4 w-4" /> {@assignment_breakdown.organization} inheriting organization default
<.icon name="hero-cloud" class="h-4 w-4" /> {@assignment_breakdown.cloud} cloud polling (no agent)
<%= if @organization.default_agent_token_id do %>

{t("Force Apply to All Devices")}

{t( "This will assign the default agent to ALL devices across all sites in this organization." )}

<% end %>
<% else %>

{t("Default Agent")}

{t("No agents configured yet.")} <.link navigate={~p"/agents"} class="text-indigo-600 hover:text-indigo-700 dark:text-indigo-400 dark:hover:text-indigo-300" > {t("Create an agent")} to enable remote polling.

<% end %> <% end %>
<.link navigate={~p"/dashboard"} class="text-sm font-semibold text-gray-900 dark:text-white" > {t("Cancel")}
<% end %> <%= if @active_tab == "members" do %>
<%= if @membership.role in [:owner, :admin] do %>

{t("Invite a Team Member")}

{t("Send an invitation email to add someone to this organization.")}

<.form for={@invite_form} id="invite-form" phx-submit="send_invitation">
<.input field={@invite_form[:email]} type="email" label={t("Email address")} placeholder="colleague@example.com" required /> <.input field={@invite_form[:role]} type="select" label={t("Role")} options={[ {"Admin", "admin"}, {"Executive", "executive"}, {"Technician", "technician"}, {"Viewer", "viewer"} ]} />
<% end %> <%= if @pending_invitations != [] do %>

{t("Pending Invitations")}

{t("Invitations that haven't been accepted yet.")}

<%= if @membership.role in [:owner, :admin] do %> <% end %> <%= if @membership.role in [:owner, :admin] do %> <% end %>
{t("Email")} {t("Role")} {t("Sent")} {t("Expires")} {t("Actions")}
{invitation.email} {invitation.role} <.timestamp datetime={invitation.inserted_at} timezone={@timezone} format="absolute" /> <.timestamp datetime={invitation.expires_at} timezone={@timezone} format="absolute" />
<% end %>

{t("Members")}

{t("People who have access to this organization.")}

<%= if @membership.role in [:owner, :admin] do %> <% end %> <%= if @membership.role in [:owner, :admin] do %> <% end %>
{t("User")} {t("Role")} {t("Joined")} {t("Actions")}
{member.user.email}
"bg-purple-100 text-purple-800 dark:bg-purple-900/30 dark:text-purple-400" :admin -> "bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-400" :executive -> "bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-400" role when role in [:technician, :member] -> "bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400" :viewer -> "bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-400" end ]}> {member.role} <.timestamp datetime={member.inserted_at} timezone={@timezone} format="absolute" /> <%= if member.role != :owner do %>
<% end %>
<% end %> <%= if @active_tab == "integrations" do %>
<%= for {provider, idx} <- Enum.with_index(@providers) do %> <%!-- 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-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" "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}

<%!-- === 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" />

{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 %> <%!-- === 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} 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. The token needs read permission at minimum." )} {t("Write permission is required if you want to push data to NetBox.")}

<%!-- 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 %> <.form for={@integration_form} id="sonar-form" phx-change="validate_integration" phx-submit="save_integration" >
<.icon name="hero-link" class="h-4 w-4 text-gray-400" />

{t("Connection")}

{t("Your Sonar instance URL and API token from the Sonar admin panel.")}

<.input field={@integration_form[:sync_interval_minutes]} type="number" label={t("Sync interval (minutes)")} min="5" value={ if(@integrations["sonar"], do: @integrations["sonar"].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-300" {:error, _} -> "text-red-800 dark:text-red-300" end ]}> {elem(@test_result, 1)}

<% end %>
<% else %> <%= if provider.id == "splynx" do %> <.form for={@integration_form} id="splynx-form" phx-change="validate_integration" phx-submit="save_integration" >
<.icon name="hero-link" class="h-4 w-4 text-gray-400" />

{t("Connection")}

{t( "Your Splynx instance URL, API key, and API secret. Find these in Splynx under Administration → API Keys." )}

<.input field={@integration_form[:sync_interval_minutes]} type="number" label={t("Sync interval (minutes)")} min="5" value={ if(@integrations["splynx"], do: @integrations["splynx"].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-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 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. Click the {t("Integrations")} tab
  3. Click {t("Add Integration")} → select {t("Events API v2")}
  4. Copy the {t("Integration Key")} and paste it above

{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. {t("Add a subscription with the Webhook URL above")}
  3. Select events: incident.resolved and incident.acknowledged
  4. Copy the {t("Signing Secret")} and paste it above
<% 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 %> <%!-- 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. Click "Add Webhook"
  3. Paste the Webhook URL above
  4. Under Events, select "All Events"
  5. Gaiia will generate a secret key — copy it
  6. Paste the secret key into the Webhook Secret field above
  7. Save the webhook

{t( "Once configured, Towerops will receive real-time updates when accounts, subscriptions, or inventory items change in Gaiia." )}

<% end %>
<% end %>
<% end %>
<% end %>