<.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

Organization Settings

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 %>

Organization Name

Update the name of your organization.

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

Default Organization

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" />

This is your default organization

<% else %>

Click to make this your default organization

<% end %>
<% end %>

Site Organization

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

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

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" />

Warning: Disabling Sites

All devices will be removed from their current sites and assigned directly to the organization. Site assignments will be lost, but devices will remain in the organization.

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

SNMP Configuration

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="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="SNMP Community String" placeholder="e.g., public" /> <% end %> <%= if @form[:snmp_version].value == "3" do %> <.input field={@form[:snmpv3_security_level]} type="select" label="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="Username" placeholder="e.g., snmpuser" /> <%= if @form[:snmpv3_security_level].value in ["authNoPriv", "authPriv"] do %> <.input field={@form[:snmpv3_auth_protocol]} type="select" label="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="Auth Password" placeholder="Min 8 characters" autocomplete="off" /> <% end %> <%= if @form[:snmpv3_security_level].value == "authPriv" do %> <.input field={@form[:snmpv3_priv_protocol]} type="select" label="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="Privacy Password" placeholder="Min 8 characters" autocomplete="off" /> <% end %> <% end %> <.input field={@form[:snmp_port]} type="number" label="SNMP Port" placeholder="161" /> <%= if @organization.snmp_community do %>

Force Apply to All Devices

This will override SNMP settings for ALL devices across all sites in this organization.

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

MikroTik API Configuration

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

Critical Security Warning: 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.

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

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

Default Agent

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="Default Remote Agent" prompt="No default agent - cloud polling" options={Enum.map(@available_agents, &{&1.name, &1.id})} />

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 %>

Force Apply to All Devices

This will assign the default agent to ALL devices across all sites in this organization.

<% end %>
<% else %>

Default Agent

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" > Create an agent to enable remote polling.

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

Invite a Team Member

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="Email address" placeholder="colleague@example.com" required />
<.input field={@invite_form[:role]} type="select" label="Role" options={[{"Admin", "admin"}, {"Member", "member"}, {"Viewer", "viewer"}]} />
<% end %> <%= if @pending_invitations != [] do %>

Pending Invitations

Invitations that haven't been accepted yet.

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

Members

People who have access to this organization.

<%= if @membership.role in [:owner, :admin] do %> <% end %> <%= if @membership.role in [:owner, :admin] do %> <% end %>
User Role Joined 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" :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 %>
"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 %>

<.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" />
<% else %> 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" />

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.

<.input field={@integration_form[:url]} type="url" label="NetBox URL" placeholder="https://netbox.example.com" autocomplete="off" value={get_credential(@integrations["netbox"], "url")} /> <.input field={@integration_form[:api_token]} type="password" label="API Token" placeholder="Enter your NetBox API token" autocomplete="off" value={get_credential(@integrations["netbox"], "api_token")} />
<%!-- Sync Direction Section --%>
<.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.

<.icon name="hero-arrow-down-tray" class="h-4 w-4 text-indigo-600" /> NetBox → TowerOps 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 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 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" />

What to Sync

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" />

Filters

(optional)

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="Sync interval (minutes)" min="5" value={ if(@integrations["netbox"], do: @integrations["netbox"].sync_interval_minutes, else: 30 ) } />

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 %> <.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" />

Connection Settings

<.input field={@integration_form[:api_key]} type="password" label={ if(provider.id == "pagerduty", do: "Integration Key (Routing Key)", else: "API Key" ) } placeholder={ if(provider.id == "pagerduty", do: "Enter your PagerDuty integration key", else: "Enter your #{provider.name} API key" ) } autocomplete="off" value={get_credential(@integrations[provider.id], "api_key")} />
<%= if provider.id == "pagerduty" do %>

<.icon name="hero-information-circle" class="h-4 w-4" /> Where to find your Integration Key

  1. In PagerDuty, go to Services → select your service (or create one)
  2. Click the Integrations tab
  3. Click Add Integration → select Events API v2
  4. Copy the Integration Key and paste it above

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" />

Two-Way Sync (Webhooks)

When someone resolves or acknowledges an incident directly in PagerDuty, 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="Webhook Signing Secret" value={ if(@integrations[provider.id], do: get_in(@integrations[provider.id].credentials, ["webhook_secret"]) || "", else: "" ) } placeholder="Optional — paste from PagerDuty webhook extension" />
  1. In PagerDuty, go to IntegrationsGeneric Webhooks (v3)
  2. Add a subscription with the Webhook URL above
  3. Select events: incident.resolved and incident.acknowledged
  4. Copy the Signing Secret and paste it above
<% end %> <%= if provider.id != "pagerduty" do %>
<.icon name="hero-clock" class="h-4 w-4 text-gray-400" />

Sync Settings

<.input field={@integration_form[:sync_interval_minutes]} type="number" label="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 %> <%!-- Gaiia Webhook Section --%> <%= if provider.id == "gaiia" && @integrations["gaiia"] do %>
<.icon name="hero-globe-alt" class="h-4 w-4 text-gray-400" />

Webhook Configuration

Receive real-time updates from Gaiia when accounts, subscriptions, or inventory items change.

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

Once configured, Towerops will receive real-time updates when accounts, subscriptions, or inventory items change in Gaiia.

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