fix: netbox url field type, gaiia ipRange→block, remove unknown webhook log

This commit is contained in:
Graham McIntire 2026-02-14 17:43:54 -06:00
parent a3730db579
commit 8752dfec49
34 changed files with 1296 additions and 1037 deletions

View file

@ -74,7 +74,7 @@ defmodule Towerops.Gaiia.Client do
edges {
node {
id
ipRange
block
}
}
}

View file

@ -99,7 +99,7 @@ defmodule Towerops.Gaiia.Sync do
defp map_network_site(node) do
ip_block_edges = get_in(node, ["ipBlocks", "edges"]) || []
ip_blocks = Enum.map(ip_block_edges, & &1["node"]["ipRange"])
ip_blocks = Enum.map(ip_block_edges, & &1["node"]["block"])
%{
gaiia_id: node["id"],

View file

@ -9,8 +9,6 @@ defmodule Towerops.Gaiia.Webhooks do
alias Towerops.Gaiia
require Logger
@doc """
Verify a Gaiia webhook signature using the `timestamp.body` signed payload format.
@ -35,7 +33,7 @@ defmodule Towerops.Gaiia.Webhooks do
"account." <> _ -> handle_account_event(organization_id, event, payload)
"billing_subscription." <> _ -> handle_subscription_event(organization_id, event, payload)
"inventory_item." <> _ -> handle_inventory_event(organization_id, event, payload)
_ -> Logger.info("Ignoring unknown Gaiia webhook event: #{event}")
_ -> :ok
end
:ok

View file

@ -23,7 +23,9 @@
/>
<div class="mt-2">
<p class="text-sm text-gray-500 dark:text-gray-400">
{t_auth("Enter the 6-digit code from your authenticator app, or use a recovery code (XXXX-XXXX format).")}
{t_auth(
"Enter the 6-digit code from your authenticator app, or use a recovery code (XXXX-XXXX format)."
)}
</p>
</div>
<div class="mt-6">

View file

@ -11,7 +11,9 @@
<.input field={f[:email]} type="email" label={t_auth("Email")} autocomplete="email" required />
<.button variant="primary" phx-disable-with={t_auth("Changing...")}>{t_auth("Change Email")}</.button>
<.button variant="primary" phx-disable-with={t_auth("Changing...")}>
{t_auth("Change Email")}
</.button>
</.form>
<div class="divider" />
@ -73,7 +75,9 @@
{session.device_os} • {session.app_version}
</p>
<p class="text-sm text-gray-500 dark:text-gray-400">
{t_auth("Last used %{date}", date: ToweropsWeb.TimeHelpers.format_date(session.last_used_at, "UTC"))}
{t_auth("Last used %{date}",
date: ToweropsWeb.TimeHelpers.format_date(session.last_used_at, "UTC")
)}
</p>
</div>
</div>

View file

@ -23,7 +23,9 @@
/>
<div class="mt-2">
<p class="text-sm text-gray-500 dark:text-gray-400">
{t_auth("Enter the 6-digit code from your authenticator app. Recovery codes are not allowed for sensitive operations.")}
{t_auth(
"Enter the 6-digit code from your authenticator app. Recovery codes are not allowed for sensitive operations."
)}
</p>
</div>
<div class="mt-6">

View file

@ -98,7 +98,9 @@
<%= if @search != "" do %>
{t("Try adjusting your search terms or clearing the filters.")}
<% else %>
{t("Your network operations log is empty. Events will appear here in real time as config changes, alerts, device events, and syncs happen across your organization.")}
{t(
"Your network operations log is empty. Events will appear here in real time as config changes, alerts, device events, and syncs happen across your organization."
)}
<% end %>
</p>
<div :if={@search != ""} class="mt-4">

View file

@ -18,7 +18,9 @@
</div>
<div class="bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10 p-6">
<h2 class="text-lg font-semibold text-gray-900 dark:text-white mb-2">{t("Organizations")}</h2>
<h2 class="text-lg font-semibold text-gray-900 dark:text-white mb-2">
{t("Organizations")}
</h2>
<p class="text-3xl font-bold text-gray-900 dark:text-white">{@org_count}</p>
<.link
navigate={~p"/admin/organizations"}
@ -91,7 +93,9 @@
<div class="bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10">
<div class="px-4 py-3 border-b border-gray-200 dark:border-white/10 flex items-center justify-between">
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">{t("Recent Audit Logs")}</h2>
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">
{t("Recent Audit Logs")}
</h2>
<.link
navigate={~p"/admin/audit"}
class="text-sm text-blue-600 dark:text-blue-400 hover:underline"

View file

@ -17,7 +17,11 @@
<button
phx-click="delete_org"
phx-value-id={org.id}
data-confirm={t("Are you sure? This will delete all sites, device, and data for this organization.")}
data-confirm={
t(
"Are you sure? This will delete all sites, device, and data for this organization."
)
}
class="text-sm text-red-600 dark:text-red-400 hover:text-red-800 dark:hover:text-red-300"
>
{t("Delete")}

View file

@ -192,7 +192,9 @@
<button
phx-click="unblock_ip"
phx-value-ip={block.ip_address}
data-confirm={t("Unblock this IP address? This will remove all offense history.")}
data-confirm={
t("Unblock this IP address? This will remove all offense history.")
}
class="text-sm text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300"
>
{t("Unblock")}

View file

@ -8,7 +8,9 @@
<div class="bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10">
<.table id="users" rows={@users}>
<:col :let={user} label={t("Email")}>{user.email}</:col>
<:col :let={user} label={t("Superuser")}>{if user.is_superuser, do: "Yes", else: "No"}</:col>
<:col :let={user} label={t("Superuser")}>
{if user.is_superuser, do: "Yes", else: "No"}
</:col>
<:col :let={user} label={t("Organizations")}>{length(user.organizations)}</:col>
<:col :let={user} label={t("Devices")}>{user.device_count || 0}</:col>
<:col :let={user} label={t("Joined")}>

View file

@ -57,8 +57,9 @@
<p class="font-medium">Performance Impact</p>
<p class="mt-1">
{t("When enabled, the server logs all SNMP data and messages for this agent.")}
{t("This generates significant log volume and should only be enabled temporarily")}
for troubleshooting.
{t(
"This generates significant log volume and should only be enabled temporarily"
)} for troubleshooting.
</p>
</div>
</div>

View file

@ -154,7 +154,11 @@
type="button"
phx-click="delete_agent"
phx-value-id={agent.id}
data-confirm={t("Are you sure you want to delete this agent? All device assignments will be removed, and devices will fall back to site/organization defaults or cloud polling.")}
data-confirm={
t(
"Are you sure you want to delete this agent? All device assignments will be removed, and devices will fall back to site/organization defaults or cloud polling."
)
}
class="text-sm font-medium text-red-600 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300"
>
{t("Delete")}
@ -175,7 +179,9 @@
{t("Cloud Pollers")}
</h2>
<p class="text-sm text-gray-600 dark:text-gray-400 mb-4">
{t("Application-wide agents that can poll devices for any organization. Only visible and manageable by superadmins.")}
{t(
"Application-wide agents that can poll devices for any organization. Only visible and manageable by superadmins."
)}
</p>
<.table
id="cloud-pollers-table"
@ -264,7 +270,11 @@
type="button"
phx-click="delete_agent"
phx-value-id={agent.id}
data-confirm={t("Are you sure you want to delete this cloud poller? This will affect all organizations using it.")}
data-confirm={
t(
"Are you sure you want to delete this cloud poller? This will affect all organizations using it."
)
}
class="text-sm font-medium text-red-600 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300"
>
{t("Delete")}
@ -286,7 +296,9 @@
{t("Global Default Cloud Poller")}
</h3>
<p class="text-sm text-amber-700 dark:text-amber-300 mb-4">
{t("Fallback agent for organizations without a default agent configured. Devices with no assignment at any level will use this agent.")}
{t(
"Fallback agent for organizations without a default agent configured. Devices with no assignment at any level will use this agent."
)}
</p>
<.form for={%{}} phx-change="update_selected_global_default" class="max-w-md">
@ -329,7 +341,9 @@
<% else %>
<p class="text-xs text-amber-600 dark:text-amber-400">
<.icon name="hero-information-circle" class="h-4 w-4 inline" />
{t("No global default configured. Devices without assignments will use direct Phoenix cluster polling.")}
{t(
"No global default configured. Devices without assignments will use direct Phoenix cluster polling."
)}
</p>
<% end %>
@ -440,7 +454,9 @@
{t("Docker Compose Setup with Auto-Updates")}
</h4>
<p class="text-xs text-gray-600 dark:text-gray-400 mb-3">
{t("This configuration includes Watchtower for automatic agent updates. The agent will automatically update to the latest version every 12 hours.")}
{t(
"This configuration includes Watchtower for automatic agent updates. The agent will automatically update to the latest version every 12 hours."
)}
</p>
<pre class="text-xs font-mono text-gray-600 dark:text-gray-400 overflow-x-auto"><code>services:
towerops-agent:

View file

@ -10,7 +10,11 @@
<button
:if={@current_scope.user.is_superuser}
phx-click="update_agent"
data-confirm={t("Are you sure you want to update this agent? It will download and replace the binary.")}
data-confirm={
t(
"Are you sure you want to update this agent? It will download and replace the binary."
)
}
class="inline-flex items-center gap-2 rounded-lg px-3.5 py-2.5 text-sm font-semibold shadow-sm focus:outline-none focus:ring-2 focus:ring-offset-2 bg-green-600 text-white hover:bg-green-700 focus:ring-green-500 dark:bg-green-500 dark:hover:bg-green-600"
>
<.icon name="hero-arrow-up-tray" class="h-5 w-5" /> Update
@ -18,7 +22,11 @@
<button
:if={@current_scope.user.is_superuser}
phx-click="restart_agent"
data-confirm={t("Are you sure you want to restart this agent? It will disconnect and restart its container.")}
data-confirm={
t(
"Are you sure you want to restart this agent? It will disconnect and restart its container."
)
}
class="inline-flex items-center gap-2 rounded-lg px-3.5 py-2.5 text-sm font-semibold shadow-sm focus:outline-none focus:ring-2 focus:ring-offset-2 bg-amber-600 text-white hover:bg-amber-700 focus:ring-amber-500 dark:bg-amber-500 dark:hover:bg-amber-600"
>
<.icon name="hero-arrow-path" class="h-5 w-5" /> Restart
@ -95,7 +103,9 @@
<%= if @agent_token.allow_remote_debug do %>
{t("Verbose logging is enabled. All SNMP data and agent messages are being logged.")}
<% else %>
{t("Verbose logging is disabled. Enable it in the agent edit page to troubleshoot issues.")}
{t(
"Verbose logging is disabled. Enable it in the agent edit page to troubleshoot issues."
)}
<% end %>
</p>
</div>

View file

@ -153,7 +153,9 @@
<div class="text-sm">
<p class="font-medium">Non-routable IP address detected</p>
<p class="mt-1">
{t("This IP address (private network or CGNAT range) cannot be reached from the cloud.")}
{t(
"This IP address (private network or CGNAT range) cannot be reached from the cloud."
)}
{t("To monitor this device, assign it to a")}
<.link navigate={~p"/agents"} class="font-medium underline">
remote agent
@ -290,7 +292,9 @@
<% end %>
<% else %>
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">
{t("Assign this device to a remote agent for local SNMP polling. Leave empty to inherit from site or organization defaults.")}
{t(
"Assign this device to a remote agent for local SNMP polling. Leave empty to inherit from site or organization defaults."
)}
</p>
<% end %>
</div>
@ -306,7 +310,9 @@
{t("SNMP Configuration")}
</h2>
<p class="mt-1 text-sm/6 text-gray-500 dark:text-gray-400">
{t("Choose monitoring mode and configure SNMP settings for detailed device information.")}
{t(
"Choose monitoring mode and configure SNMP settings for detailed device information."
)}
</p>
</div>
@ -349,8 +355,9 @@
<div class="mt-2 text-sm text-gray-600 dark:text-gray-400">
<%= if @monitoring_mode == "snmp_and_icmp" do %>
<p>
{t("Monitor device availability with ICMP pings and collect detailed information via SNMP")}
(interfaces, sensors, system info).
{t(
"Monitor device availability with ICMP pings and collect detailed information via SNMP"
)} (interfaces, sensors, system info).
</p>
<% else %>
<p>Monitor device availability with ICMP pings only (no SNMP discovery).</p>
@ -418,7 +425,9 @@
<% :default -> %>
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400 flex items-center gap-1">
<.icon name="hero-information-circle" class="h-4 w-4" />
{t("Using default (v2c, no community set - configure at organization or site level)")}
{t(
"Using default (v2c, no community set - configure at organization or site level)"
)}
</p>
<% end %>
<% end %>
@ -538,7 +547,11 @@
<div class="col-span-full">
<div class="flex gap-3">
<.button type="button" phx-click="test_snmp" phx-disable-with={t("Testing...")}>
<.button
type="button"
phx-click="test_snmp"
phx-disable-with={t("Testing...")}
>
{t("Test SNMP Connection")}
</.button>
@ -623,7 +636,9 @@
{t("MikroTik API Configuration")}
</h2>
<p class="mt-1 text-sm/6 text-gray-500 dark:text-gray-400">
{t("Configure MikroTik RouterOS API access. Works alongside SNMP for enhanced device management. SSH is used for device backups.")}
{t(
"Configure MikroTik RouterOS API access. Works alongside SNMP for enhanced device management. SSH is used for device backups."
)}
</p>
<div class="mt-3 rounded-md bg-blue-50 dark:bg-blue-900/20 p-3">
<p class="text-xs text-blue-700 dark:text-blue-300">
@ -694,8 +709,12 @@
<div class="rounded-md bg-red-50 dark:bg-red-900/20 p-4">
<p class="text-sm text-red-700 dark:text-red-300">
<strong>🚨 Critical Security Warning:</strong>
{t("Plain API (port 8728) sends credentials unencrypted over the network.")}
<strong>{t("This setting is blocked when using cloud pollers")}</strong>
{t(
"Plain API (port 8728) sends credentials unencrypted over the network."
)}
<strong>
{t("This setting is blocked when using cloud pollers")}
</strong>
and should only be used with local agents on trusted networks.
</p>
</div>
@ -726,7 +745,9 @@
<%= if @live_action == :edit do %>
<div class="grid max-w-7xl grid-cols-1 gap-x-8 gap-y-6 px-4 py-8 sm:px-6 md:grid-cols-3 lg:px-8 border-t border-gray-200 dark:border-white/10">
<div>
<h2 class="text-base/7 font-semibold text-red-600 dark:text-red-500">{t("Danger Zone")}</h2>
<h2 class="text-base/7 font-semibold text-red-600 dark:text-red-500">
{t("Danger Zone")}
</h2>
<p class="mt-1 text-sm/6 text-gray-500 dark:text-gray-400">
{t("Permanently delete this device and all its data.")}
</p>
@ -735,11 +756,17 @@
<div class="md:col-span-2">
<div class="rounded-md bg-red-50 dark:bg-red-900/20 p-4 border border-red-200 dark:border-red-800">
<p class="text-sm text-red-800 dark:text-red-200 mb-4">
{t("Once you delete this device, there is no going back. All monitoring history and alerts will be permanently deleted.")}
{t(
"Once you delete this device, there is no going back. All monitoring history and alerts will be permanently deleted."
)}
</p>
<.button
phx-click="delete"
data-confirm={t("Are you sure you want to delete this device? All monitoring history and alerts will be permanently deleted.")}
data-confirm={
t(
"Are you sure you want to delete this device? All monitoring history and alerts will be permanently deleted."
)
}
variant="danger"
>
<.icon name="hero-trash" class="h-4 w-4" /> Delete Device

View file

@ -97,7 +97,9 @@
<.button
type="button"
phx-click="force_rediscover_all"
data-confirm={t("This will trigger SNMP discovery for all SNMP-enabled devices. Continue?")}
data-confirm={
t("This will trigger SNMP discovery for all SNMP-enabled devices. Continue?")
}
>
<.icon name="hero-magnifying-glass" class="h-4 w-4" /> Force Rediscover All
</.button>
@ -156,7 +158,9 @@
</h3>
<div class="mt-2 text-sm text-blue-700 dark:text-blue-300">
<p>
{t("Sites help you organize devices by physical location. Create a site to assign your devices and keep things organized.")}
{t(
"Sites help you organize devices by physical location. Create a site to assign your devices and keep things organized."
)}
</p>
</div>
<div class="mt-4">
@ -172,7 +176,9 @@
<%= if !@has_devices do %>
<div class="text-center py-16">
<.icon name="hero-server" class="mx-auto h-12 w-12 text-gray-400 dark:text-gray-500" />
<h3 class="mt-4 text-lg font-semibold text-gray-900 dark:text-white">{t("No devices")}</h3>
<h3 class="mt-4 text-lg font-semibold text-gray-900 dark:text-white">
{t("No devices")}
</h3>
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">
{t("Get started by adding your first device.")}
</p>

View file

@ -886,7 +886,9 @@
<%= if @voltage_sensors && length(@voltage_sensors) > 0 do %>
<div class="bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10">
<div class="px-4 py-3 border-b border-gray-200 dark:border-white/10">
<h3 class="text-sm font-semibold text-gray-900 dark:text-white">{t("Voltage")}</h3>
<h3 class="text-sm font-semibold text-gray-900 dark:text-white">
{t("Voltage")}
</h3>
</div>
<div class="p-4">
<dl class="space-y-1">
@ -964,7 +966,9 @@
<%= if (@general_counters && length(@general_counters) > 0) || (@firewall_counters && length(@firewall_counters) > 0) do %>
<div class="bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10">
<div class="px-4 py-3 border-b border-gray-200 dark:border-white/10">
<h3 class="text-sm font-semibold text-gray-900 dark:text-white">{t("Counters")}</h3>
<h3 class="text-sm font-semibold text-gray-900 dark:text-white">
{t("Counters")}
</h3>
</div>
<div class="p-4 space-y-4">
<!-- General Counters -->
@ -1037,7 +1041,9 @@
<%= if @wireless_sensors && length(@wireless_sensors) > 0 do %>
<div class="bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10">
<div class="px-4 py-3 border-b border-gray-200 dark:border-white/10">
<h3 class="text-sm font-semibold text-gray-900 dark:text-white">{t("Wireless")}</h3>
<h3 class="text-sm font-semibold text-gray-900 dark:text-white">
{t("Wireless")}
</h3>
</div>
<div class="p-4">
<dl class="space-y-1">
@ -1406,7 +1412,9 @@
{t("No neighbors discovered")}
</h3>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
{t("This device doesn't have any LLDP or CDP neighbors, or neighbor discovery hasn't run yet.")}
{t(
"This device doesn't have any LLDP or CDP neighbors, or neighbor discovery hasn't run yet."
)}
</p>
</div>
</div>
@ -1572,7 +1580,9 @@
{t("No MAC addresses discovered")}
</h3>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
{t("This device doesn't have any MAC forwarding entries, or MAC discovery hasn't run yet.")}
{t(
"This device doesn't have any MAC forwarding entries, or MAC discovery hasn't run yet."
)}
</p>
</div>
</div>
@ -1641,7 +1651,9 @@
{t("No VLANs discovered")}
</h3>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
{t("This device doesn't have any VLANs configured, or VLAN discovery hasn't run yet.")}
{t(
"This device doesn't have any VLANs configured, or VLAN discovery hasn't run yet."
)}
</p>
</div>
</div>
@ -1757,7 +1769,9 @@
{t("No IP addresses discovered")}
</h3>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
{t("This device doesn't have any IP addresses discovered, or IP discovery hasn't run yet.")}
{t(
"This device doesn't have any IP addresses discovered, or IP discovery hasn't run yet."
)}
</p>
</div>
</div>
@ -1903,7 +1917,11 @@
<button
phx-click="delete_backup"
phx-value-id={backup.id}
data-confirm={t("Are you sure you want to delete this backup? This action cannot be undone.")}
data-confirm={
t(
"Are you sure you want to delete this backup? This action cannot be undone."
)
}
class="inline-flex items-center gap-1 px-2 py-1 text-xs font-medium text-red-600 hover:text-red-800 dark:text-red-400 dark:hover:text-red-300"
>
<.icon name="hero-trash" class="h-4 w-4" /> Delete
@ -2069,7 +2087,9 @@
{t("No events yet")}
</h3>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
{t("Events will appear here as interface states change, speeds change, or other monitored changes occur.")}
{t(
"Events will appear here as interface states change, speeds change, or other monitored changes occur."
)}
</p>
</div>
<% end %>
@ -2242,7 +2262,9 @@
<% else %>
<div class="mt-5 p-4 bg-gray-50 dark:bg-gray-900 rounded-md">
<p class="text-sm text-gray-500 dark:text-gray-400">
{t("No raw discovery data available. Run discovery again to collect debug data.")}
{t(
"No raw discovery data available. Run discovery again to collect debug data."
)}
</p>
</div>
<% end %>
@ -2258,7 +2280,9 @@
{t("No checks configured")}
</h3>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
{t("Run discovery to automatically detect sensors, interfaces, and other monitorable items, or add a service check manually.")}
{t(
"Run discovery to automatically detect sensors, interfaces, and other monitorable items, or add a service check manually."
)}
</p>
<div class="mt-6 flex justify-center gap-3">
<%= if @device.snmp_enabled do %>

View file

@ -15,7 +15,9 @@
{t("Gaiia Entity Mapping")}
</h1>
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
{t("Link Gaiia entities to your Towerops sites and devices. Match suggestions are shown in amber.")}
{t(
"Link Gaiia entities to your Towerops sites and devices. Match suggestions are shown in amber."
)}
</p>
</div>
@ -120,7 +122,9 @@
<% "unmapped" -> %>
{t("All entities are mapped.")}
<% _ -> %>
{t("No Gaiia data has been synced yet. Enable the Gaiia integration and run a sync.")}
{t(
"No Gaiia data has been synced yet. Enable the Gaiia integration and run a sync."
)}
<% end %>
</p>
</div>

View file

@ -6,7 +6,9 @@
<.header>
{t("Gaiia Inventory Reconciliation")}
<:subtitle>
{t("Compare Gaiia inventory against Towerops device discovery. Reconciliation runs nightly.")}
{t(
"Compare Gaiia inventory against Towerops device discovery. Reconciliation runs nightly."
)}
</:subtitle>
</.header>

View file

@ -266,7 +266,9 @@
{t("Webhook Configuration")}
</h4>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
{t("Receive real-time updates from Gaiia when accounts, subscriptions, or inventory items change.")}
{t(
"Receive real-time updates from Gaiia when accounts, subscriptions, or inventory items change."
)}
</p>
<div class="mt-4 space-y-4">
@ -321,7 +323,11 @@
type="button"
id="regenerate-webhook-secret"
phx-click="regenerate_webhook_secret"
data-confirm={t("Are you sure? Any existing Gaiia webhook using the current secret will stop working.")}
data-confirm={
t(
"Are you sure? Any existing Gaiia webhook using the current secret will stop working."
)
}
class="text-xs text-red-600 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300"
>
{t("Regenerate Secret")}
@ -342,7 +348,9 @@
<li>Save the webhook</li>
</ol>
<p class="mt-2 text-xs text-blue-600 dark:text-blue-400">
{t("Once configured, Towerops will receive real-time updates when accounts, subscriptions, or inventory items change in Gaiia.")}
{t(
"Once configured, Towerops will receive real-time updates when accounts, subscriptions, or inventory items change in Gaiia."
)}
</p>
</div>
</div>

View file

@ -107,7 +107,9 @@
<% "matched" -> %>
{t("No matched access points yet. Sync from Preseem and link devices.")}
<% _ -> %>
{t("No Preseem access points have been synced yet. Enable the integration and run a sync.")}
{t(
"No Preseem access points have been synced yet. Enable the integration and run a sync."
)}
<% end %>
</p>
</div>

View file

@ -139,14 +139,16 @@ defmodule ToweropsWeb.Org.SettingsLive do
def handle_event("apply_snmp_to_all", _params, socket) do
{count, _} = Organizations.apply_snmp_config_to_all_equipment(socket.assigns.organization.id)
{:noreply, put_flash(socket, :info, t("Applied SNMP configuration to %{count} device records across all sites", count: count))}
{:noreply,
put_flash(socket, :info, t("Applied SNMP configuration to %{count} device records across all sites", count: count))}
end
@impl true
def handle_event("apply_agent_to_all", _params, socket) do
{count, _} = Organizations.apply_agent_to_all_equipment(socket.assigns.organization.id)
{:noreply, put_flash(socket, :info, t("Applied default agent to %{count} device records across all sites", count: count))}
{:noreply,
put_flash(socket, :info, t("Applied default agent to %{count} device records across all sites", count: count))}
end
# === Members tab events ===
@ -176,7 +178,8 @@ defmodule ToweropsWeb.Org.SettingsLive do
|> assign(:invite_form, to_form(%{"email" => "", "role" => "member"}))}
{:error, changeset} ->
{:noreply, put_flash(socket, :error, t("Failed to send invitation: %{errors}", errors: error_messages(changeset)))}
{:noreply,
put_flash(socket, :error, t("Failed to send invitation: %{errors}", errors: error_messages(changeset)))}
end
end

View file

@ -131,7 +131,9 @@
{t("Default Organization")}
</h2>
<p class="mt-1 text-sm/6 text-gray-500 dark:text-gray-400">
{t("Set this organization as your default. When you log in, you'll be directed to your default organization.")}
{t(
"Set this organization as your default. When you log in, you'll be directed to your default organization."
)}
</p>
</div>
@ -171,7 +173,9 @@
{t("Site Organization")}
</h2>
<p class="mt-1 text-sm/6 text-gray-500 dark:text-gray-400">
{t("Enable site organization to group devices into physical locations (offices, datacenters, etc.).")}
{t(
"Enable site organization to group devices into physical locations (offices, datacenters, etc.)."
)}
</p>
</div>
@ -182,7 +186,9 @@
label={t("Use sites to organize devices")}
/>
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
{t("When enabled, you can organize devices into sites. When disabled, all devices belong directly to the organization.")}
{t(
"When enabled, you can organize devices into sites. When disabled, all devices belong directly to the organization."
)}
</p>
<%= if @organization.use_sites && @form[:use_sites].value == false do %>
@ -197,8 +203,12 @@
</h3>
<div class="mt-2 text-sm text-amber-700 dark:text-amber-300">
<p>
{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.")}
{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."
)}
</p>
</div>
</div>
@ -217,7 +227,9 @@
{t("SNMP Configuration")}
</h2>
<p class="mt-1 text-sm/6 text-gray-500 dark:text-gray-400">
{t("Set default SNMP settings for all devices in this organization. These can be overridden at the site or device level.")}
{t(
"Set default SNMP settings for all devices in this organization. These can be overridden at the site or device level."
)}
</p>
<p class="mt-3 text-xs text-gray-500 dark:text-gray-400 italic">
<.icon name="hero-information-circle" class="h-4 w-4 inline" />
@ -330,12 +342,18 @@
{t("Force Apply to All Devices")}
</h3>
<p class="text-sm text-amber-700 dark:text-amber-300 mb-3">
{t("This will override SNMP settings for ALL devices across all sites in this organization.")}
{t(
"This will override SNMP settings for ALL devices across all sites in this organization."
)}
</p>
<button
type="button"
phx-click="apply_snmp_to_all"
data-confirm={t("This will replace SNMP settings for ALL devices in this organization. Are you sure?")}
data-confirm={
t(
"This will replace SNMP settings for ALL devices in this organization. Are you sure?"
)
}
class="rounded-md bg-red-600 px-3 py-2 text-sm font-semibold text-white shadow-xs hover:bg-red-500 dark:bg-red-500 dark:shadow-none dark:hover:bg-red-400"
>
<.icon name="hero-arrow-path" class="h-4 w-4 inline" />
@ -355,7 +373,9 @@
{t("MikroTik API Configuration")}
</h2>
<p class="mt-1 text-sm/6 text-gray-500 dark:text-gray-400">
{t("Set default MikroTik RouterOS API credentials for all devices in this organization. Only applies to devices detected as MikroTik.")}
{t(
"Set default MikroTik RouterOS API credentials for all devices in this organization. Only applies to devices detected as MikroTik."
)}
</p>
<div class="mt-3 rounded-md bg-blue-50 dark:bg-blue-900/20 p-3">
<p class="text-xs text-blue-700 dark:text-blue-300">
@ -419,7 +439,9 @@
<div class="rounded-md bg-red-50 dark:bg-red-900/20 p-4">
<p class="text-sm text-red-700 dark:text-red-300">
<strong>{t("Critical Security Warning:")}</strong>
{t("Plain API (port 8728) sends credentials unencrypted over the network. This setting is")}
{t(
"Plain API (port 8728) sends credentials unencrypted over the network. This setting is"
)}
<strong>blocked for devices using cloud pollers</strong>
and should only be used with local agents on trusted networks.
</p>
@ -430,7 +452,9 @@
<div class="rounded-md bg-yellow-50 dark:bg-yellow-900/20 p-4">
<p class="text-sm text-yellow-700 dark:text-yellow-300">
<strong>{t("Security Warning:")}</strong>
{t("Plain API (port 8728) sends credentials unencrypted. Use SSL (port 8729) whenever possible.")}
{t(
"Plain API (port 8728) sends credentials unencrypted. Use SSL (port 8729) whenever possible."
)}
</p>
</div>
</div>
@ -447,7 +471,9 @@
{t("Default Agent")}
</h2>
<p class="mt-1 text-sm/6 text-gray-500 dark:text-gray-400">
{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.")}
{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."
)}
</p>
<p class="mt-3 text-xs text-gray-500 dark:text-gray-400 italic">
<.icon name="hero-information-circle" class="h-4 w-4 inline" />
@ -503,12 +529,18 @@
{t("Force Apply to All Devices")}
</h3>
<p class="text-sm text-amber-700 dark:text-amber-300 mb-3">
{t("This will assign the default agent to ALL devices across all sites in this organization.")}
{t(
"This will assign the default agent to ALL devices across all sites in this organization."
)}
</p>
<button
type="button"
phx-click="apply_agent_to_all"
data-confirm={t("This will replace agent assignments for ALL devices in this organization. Are you sure?")}
data-confirm={
t(
"This will replace agent assignments for ALL devices in this organization. Are you sure?"
)
}
class="rounded-md bg-red-600 px-3 py-2 text-sm font-semibold text-white shadow-xs hover:bg-red-500 dark:bg-red-500 dark:shadow-none dark:hover:bg-red-400"
>
<.icon name="hero-arrow-path" class="h-4 w-4 inline" />
@ -1097,7 +1129,9 @@
</h4>
</div>
<p class="text-xs text-gray-500 dark:text-gray-400 mb-4 ml-6">
{t("Your NetBox instance URL and API token. The token needs read permission at minimum.")}
{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.")}
</p>
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
@ -1106,7 +1140,7 @@
{t("NetBox URL")}
</label>
<input
type="url"
type="text"
name="integration[url]"
value={get_credential(@integrations["netbox"], "url")}
placeholder="https://netbox.example.com"
@ -1139,7 +1173,9 @@
</h4>
</div>
<p class="text-xs text-gray-500 dark:text-gray-400 mb-4 ml-6">
{t("Choose how data flows between TowerOps and NetBox. You can change this later.")}
{t(
"Choose how data flows between TowerOps and NetBox. You can change this later."
)}
</p>
<input
type="hidden"
@ -1167,7 +1203,9 @@
NetBox → TowerOps
</span>
<span class="mt-1.5 text-xs text-gray-500 dark:text-gray-400">
{t("NetBox is the source of truth. Import devices, sites, and IPs from NetBox into TowerOps.")}
{t(
"NetBox is the source of truth. Import devices, sites, and IPs from NetBox into TowerOps."
)}
</span>
</div>
</div>
@ -1191,7 +1229,9 @@
TowerOps → NetBox
</span>
<span class="mt-1.5 text-xs text-gray-500 dark:text-gray-400">
{t("TowerOps is the source of truth. Push discovered devices and monitoring data to NetBox.")}
{t(
"TowerOps is the source of truth. Push discovered devices and monitoring data to NetBox."
)}
</span>
</div>
</div>
@ -1217,7 +1257,9 @@
/> Bidirectional
</span>
<span class="mt-1.5 text-xs text-gray-500 dark:text-gray-400">
{t("Merge data from both systems. Conflicts resolved by most-recently-updated. Requires write token.")}
{t(
"Merge data from both systems. Conflicts resolved by most-recently-updated. Requires write token."
)}
</span>
</div>
</div>
@ -1233,7 +1275,9 @@
</h4>
</div>
<p class="text-xs text-gray-500 dark:text-gray-400 mb-4 ml-6">
{t("Choose which NetBox objects participate in sync. Devices and sites are recommended at minimum.")}
{t(
"Choose which NetBox objects participate in sync. Devices and sites are recommended at minimum."
)}
</p>
<div class="grid grid-cols-1 gap-3 sm:grid-cols-2">
<label class="flex items-start gap-3 rounded-lg border border-gray-200 p-4 transition-colors hover:bg-gray-50 dark:border-white/10 dark:hover:bg-white/5">
@ -1339,7 +1383,9 @@
</span>
</div>
<p class="text-xs text-gray-500 dark:text-gray-400 mb-4 ml-6">
{t("Narrow the sync scope. Leave blank to sync everything. Comma-separated for multiple values.")}
{t(
"Narrow the sync scope. Leave blank to sync everything. Comma-separated for multiple values."
)}
</p>
<div class="grid grid-cols-1 gap-4 sm:grid-cols-3">
<div>
@ -1397,7 +1443,9 @@
}
/>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
{t("How often TowerOps checks NetBox for changes. 30 minutes is recommended for most setups.")}
{t(
"How often TowerOps checks NetBox for changes. 30 minutes is recommended for most setups."
)}
</p>
</div>
</div>
@ -1503,10 +1551,14 @@
Click <strong>{t("Add Integration")}</strong>
→ select <strong>{t("Events API v2")}</strong>
</li>
<li>Copy the <strong>{t("Integration Key")}</strong> and paste it above</li>
<li>
Copy the <strong>{t("Integration Key")}</strong> and paste it above
</li>
</ol>
<p class="mt-2.5 text-xs text-blue-700 dark:text-blue-400 ml-5">
{t("When connected, TowerOps will automatically trigger, acknowledge, and resolve PagerDuty incidents in sync with your alerts.")}
{t(
"When connected, TowerOps will automatically trigger, acknowledge, and resolve PagerDuty incidents in sync with your alerts."
)}
</p>
</div>
@ -1518,7 +1570,9 @@
</h4>
</div>
<p class="text-xs text-gray-500 dark:text-gray-400 mb-3">
{t("When someone resolves or acknowledges an incident directly in PagerDuty,")}
{t(
"When someone resolves or acknowledges an incident directly in PagerDuty,"
)}
{t("TowerOps will automatically update the corresponding alert.")}
</p>
<div class="mb-3">
@ -1652,7 +1706,9 @@
</h4>
</div>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400 ml-6">
{t("Receive real-time updates from Gaiia when accounts, subscriptions, or inventory items change.")}
{t(
"Receive real-time updates from Gaiia when accounts, subscriptions, or inventory items change."
)}
</p>
<div class="mt-4 space-y-4">
@ -1714,7 +1770,9 @@
<li>Save the webhook</li>
</ol>
<p class="mt-2.5 text-xs text-blue-600 dark:text-blue-400 ml-5">
{t("Once configured, Towerops will receive real-time updates when accounts, subscriptions, or inventory items change in Gaiia.")}
{t(
"Once configured, Towerops will receive real-time updates when accounts, subscriptions, or inventory items change in Gaiia."
)}
</p>
</div>
</div>

View file

@ -30,7 +30,9 @@
<div :if={@organizations == []} class="text-center py-16">
<.icon name="hero-building-office" class="mx-auto h-12 w-12 text-gray-400 dark:text-gray-600" />
<h3 class="mt-4 text-lg font-semibold text-gray-900 dark:text-white">{t("No organizations")}</h3>
<h3 class="mt-4 text-lg font-semibold text-gray-900 dark:text-white">
{t("No organizations")}
</h3>
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">
{t("Get started by creating a new organization.")}
</p>

View file

@ -18,8 +18,9 @@
<p class="mt-1 text-sm text-orange-700 dark:text-orange-300">
You already have {@free_org_count} free {if @free_org_count == 1,
do: "organization",
else: "organizations"}.
{t("To create additional organizations, you'll need to upgrade your existing organization to a paid plan.")}
else: "organizations"}. {t(
"To create additional organizations, you'll need to upgrade your existing organization to a paid plan."
)}
</p>
</div>
</div>
@ -31,7 +32,9 @@
<.input field={@form[:name]} type="text" label={t("Organization Name")} required />
<div class="flex gap-3 mt-6">
<.button phx-disable-with={t("Creating...")} variant="primary">Create Organization</.button>
<.button phx-disable-with={t("Creating...")} variant="primary">
Create Organization
</.button>
<.button navigate={~p"/orgs"}>{t("Cancel")}</.button>
</div>
</.form>

View file

@ -98,7 +98,9 @@
</button>
</div>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
{t("Enter a full address and click Geocode to automatically fill latitude/longitude")}
{t(
"Enter a full address and click Geocode to automatically fill latitude/longitude"
)}
</p>
</label>
</div>
@ -192,7 +194,9 @@
<span class="text-gray-400 dark:text-gray-500 font-normal">(optional)</span>
</h3>
<p class="text-sm text-gray-600 dark:text-gray-400 mb-4">
{t("Set a default agent for SNMP polling at this site. This will override the organization default for all devices at this site. Leave blank to inherit from organization.")}
{t(
"Set a default agent for SNMP polling at this site. This will override the organization default for all devices at this site. Leave blank to inherit from organization."
)}
</p>
<.input
@ -220,7 +224,11 @@
<.button
type="button"
phx-click="apply_agent_to_all"
data-confirm={t("This will replace agent assignments for ALL devices at this site. Are you sure?")}
data-confirm={
t(
"This will replace agent assignments for ALL devices at this site. Are you sure?"
)
}
variant="danger"
>
<.icon name="hero-arrow-path" class="h-4 w-4" /> Apply Agent to All Device
@ -234,7 +242,9 @@
</p>
<% else %>
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400">
{t("Set a default agent for all devices at this site. Device can override this setting individually.")}
{t(
"Set a default agent for all devices at this site. Device can override this setting individually."
)}
</p>
<% end %>
<% end %>
@ -247,7 +257,9 @@
<span class="text-gray-400 dark:text-gray-500 font-normal">(optional)</span>
</h3>
<p class="text-sm text-gray-600 dark:text-gray-400 mb-4">
{t("Override organization SNMP defaults for all devices at this site. Leave blank to inherit from organization.")}
{t(
"Override organization SNMP defaults for all devices at this site. Leave blank to inherit from organization."
)}
</p>
<.input
@ -359,7 +371,9 @@
<.button
type="button"
phx-click="apply_snmp_to_all"
data-confirm={t("This will replace SNMP settings for ALL devices at this site. Are you sure?")}
data-confirm={
t("This will replace SNMP settings for ALL devices at this site. Are you sure?")
}
variant="danger"
>
<.icon name="hero-arrow-path" class="h-4 w-4" /> Apply SNMP Config to All Device
@ -375,7 +389,9 @@
<span class="text-gray-400 dark:text-gray-500 font-normal">(optional)</span>
</h3>
<p class="text-sm text-gray-600 dark:text-gray-400 mb-4">
{t("Override organization MikroTik API defaults for all devices at this site. Leave blank to inherit from organization. Only applies to MikroTik devices.")}
{t(
"Override organization MikroTik API defaults for all devices at this site. Leave blank to inherit from organization. Only applies to MikroTik devices."
)}
</p>
<div class="mb-4 rounded-md bg-blue-50 dark:bg-blue-900/20 p-3">
<p class="text-xs text-blue-700 dark:text-blue-300">
@ -423,7 +439,9 @@
<div class="mt-3 rounded-md bg-red-50 dark:bg-red-900/20 p-4">
<p class="text-sm text-red-700 dark:text-red-300">
<strong>🚨 Critical Security Warning:</strong>
{t("Plain API (port 8728) sends credentials unencrypted over the network. This setting is")}
{t(
"Plain API (port 8728) sends credentials unencrypted over the network. This setting is"
)}
<strong>blocked for devices using cloud pollers</strong>
and should only be used with local agents on trusted networks.
</p>
@ -432,7 +450,9 @@
<div class="mt-3 rounded-md bg-yellow-50 dark:bg-yellow-900/20 p-4">
<p class="text-sm text-yellow-700 dark:text-yellow-300">
<strong>⚠️ Security Warning:</strong>
{t("Plain API (port 8728) sends credentials unencrypted. Use SSL (port 8729) whenever possible.")}
{t(
"Plain API (port 8728) sends credentials unencrypted. Use SSL (port 8729) whenever possible."
)}
</p>
</div>
<% end %>
@ -448,13 +468,21 @@
<%= if @live_action == :edit do %>
<div class="mt-12 pt-8 border-t border-gray-200 dark:border-white/10">
<h3 class="text-lg font-semibold text-red-600 dark:text-red-500 mb-4">{t("Danger Zone")}</h3>
<h3 class="text-lg font-semibold text-red-600 dark:text-red-500 mb-4">
{t("Danger Zone")}
</h3>
<p class="text-sm text-gray-600 dark:text-gray-400 mb-4">
{t("Once you delete a site, there is no going back. This will also delete all devices at this site.")}
{t(
"Once you delete a site, there is no going back. This will also delete all devices at this site."
)}
</p>
<.button
phx-click="delete"
data-confirm={t("Are you sure you want to delete this site? This will also delete all devices at this site.")}
data-confirm={
t(
"Are you sure you want to delete this site? This will also delete all devices at this site."
)
}
variant="danger"
>
<.icon name="hero-trash" class="h-4 w-4" /> Delete Site

View file

@ -16,7 +16,9 @@
<.button
type="button"
phx-click="force_rediscover_all"
data-confirm={t("This will trigger SNMP discovery for all devices at this site. Continue?")}
data-confirm={
t("This will trigger SNMP discovery for all devices at this site. Continue?")
}
>
<.icon name="hero-magnifying-glass" class="h-4 w-4" /> Force Rediscover All
</.button>
@ -148,7 +150,9 @@
</dl>
<%= if @site.child_sites != [] do %>
<div class="mt-4 border-t border-base-300 pt-3">
<h4 class="text-sm font-semibold text-gray-900 dark:text-white">{t("Child Sites")}</h4>
<h4 class="text-sm font-semibold text-gray-900 dark:text-white">
{t("Child Sites")}
</h4>
<ul class="mt-2 space-y-1">
<li :for={child <- @site.child_sites}>
<.link navigate={~p"/sites/#{child.id}"} class="link link-primary text-sm">

View file

@ -18,7 +18,11 @@
type="text"
name="query"
value={@query}
placeholder={t("Search by customer name, IP address, account ID, serial number, or device name...")}
placeholder={
t(
"Search by customer name, IP address, account ID, serial number, or device name..."
)
}
class="input input-bordered w-full dark:bg-gray-800 dark:border-white/10 dark:text-white"
phx-debounce="300"
autofocus
@ -534,9 +538,13 @@
name="hero-magnifying-glass-circle"
class="h-16 w-16 mx-auto mb-4 text-gray-300 dark:text-gray-600"
/>
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-2">{t("Subscriber Trace")}</h2>
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-2">
{t("Subscriber Trace")}
</h2>
<p class="text-gray-500 dark:text-gray-400 max-w-md mx-auto">
{t("Search for a customer by name, IP address, account ID, or device name to see their full network status at a glance.")}
{t(
"Search for a customer by name, IP address, account ID, or device name to see their full network status at a glance."
)}
</p>
</div>
</div>

View file

@ -332,7 +332,9 @@
{t("Default Organization")}
</label>
<p class="mt-1 text-sm/6 text-gray-500 dark:text-gray-400">
{t("Choose which organization you see by default when logging in or navigating to pages without an organization context.")}
{t(
"Choose which organization you see by default when logging in or navigating to pages without an organization context."
)}
</p>
<div class="mt-2 grid grid-cols-1">
<select
@ -385,9 +387,13 @@
<!-- Email Address Section -->
<div class="grid max-w-7xl grid-cols-1 gap-x-8 gap-y-10 px-4 py-16 sm:px-6 md:grid-cols-3 lg:px-8">
<div>
<h2 class="text-base/7 font-semibold text-gray-900 dark:text-white">{t("Email Address")}</h2>
<h2 class="text-base/7 font-semibold text-gray-900 dark:text-white">
{t("Email Address")}
</h2>
<p class="mt-1 text-sm/6 text-gray-500 dark:text-gray-400">
{t("Update your email address. You'll receive a confirmation link to verify the new address.")}
{t(
"Update your email address. You'll receive a confirmation link to verify the new address."
)}
</p>
</div>
@ -435,7 +441,9 @@
{t("Change Password")}
</h2>
<p class="mt-1 text-sm/6 text-gray-500 dark:text-gray-400">
{t("Update your password. You'll be logged out and need to sign in again with your new password.")}
{t(
"Update your password. You'll be logged out and need to sign in again with your new password."
)}
</p>
</div>
@ -526,9 +534,13 @@
<!-- API Tokens Section -->
<div class="grid max-w-7xl grid-cols-1 gap-x-8 gap-y-10 px-4 py-16 sm:px-6 md:grid-cols-3 lg:px-8">
<div>
<h2 class="text-base/7 font-semibold text-gray-900 dark:text-white">{t("API Tokens")}</h2>
<h2 class="text-base/7 font-semibold text-gray-900 dark:text-white">
{t("API Tokens")}
</h2>
<p class="mt-1 text-sm/6 text-gray-500 dark:text-gray-400">
{t("Create API tokens to access Towerops programmatically. Tokens are scoped to a specific organization.")}
{t(
"Create API tokens to access Towerops programmatically. Tokens are scoped to a specific organization."
)}
</p>
</div>
@ -583,7 +595,11 @@
type="button"
phx-click="delete_api_token"
phx-value-token-id={token.id}
data-confirm={t("Are you sure you want to delete this API token? This action cannot be undone.")}
data-confirm={
t(
"Are you sure you want to delete this API token? This action cannot be undone."
)
}
class="rounded-md bg-white px-2.5 py-1.5 text-sm font-semibold text-gray-900 shadow-xs inset-ring-1 inset-ring-gray-300 hover:bg-gray-100 dark:bg-white/10 dark:text-white dark:shadow-none dark:inset-ring-white/10 dark:hover:bg-white/20"
>
{t("Delete")}
@ -723,8 +739,9 @@
</h3>
<div class="mt-2 text-sm text-amber-700 dark:text-amber-300">
<p>
We detected {@failed_login_count} failed login attempts in the last 24 hours.
{t("Please review your login history below and ensure all activity is authorized.")}
We detected {@failed_login_count} failed login attempts in the last 24 hours. {t(
"Please review your login history below and ensure all activity is authorized."
)}
</p>
</div>
</div>
@ -1035,7 +1052,9 @@
{t("Authenticator Apps")}
</h2>
<p class="mt-1 text-sm/6 text-gray-500 dark:text-gray-400">
{t("Manage TOTP devices for two-factor authentication. You must have at least one device configured.")}
{t(
"Manage TOTP devices for two-factor authentication. You must have at least one device configured."
)}
</p>
</div>
@ -1160,7 +1179,9 @@
<button
type="button"
phx-click="regenerate_recovery_codes"
data-confirm={t("This will invalidate all existing unused recovery codes. Continue?")}
data-confirm={
t("This will invalidate all existing unused recovery codes. Continue?")
}
class="inline-flex items-center gap-x-1.5 rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-xs hover:bg-indigo-500 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 dark:bg-indigo-500 dark:hover:bg-indigo-400"
>
<.icon name="hero-arrow-path" class="-ml-0.5 h-5 w-5" /> Regenerate
@ -1181,8 +1202,9 @@
</h3>
<div class="mt-2 text-sm text-amber-700 dark:text-amber-300">
<p>
{t("You have no recovery codes available. If you lose access to your authenticator app,")}
you won't be able to log in. Generate new codes now.
{t(
"You have no recovery codes available. If you lose access to your authenticator app,"
)} you won't be able to log in. Generate new codes now.
</p>
</div>
</div>
@ -1233,7 +1255,9 @@
</h3>
<div class="mt-2">
<p class="text-sm text-gray-500 dark:text-gray-400">
{t("This will log you out from all other browsers and devices. Your current session will remain active.")}
{t(
"This will log you out from all other browsers and devices. Your current session will remain active."
)}
</p>
</div>
</div>
@ -1296,7 +1320,9 @@
</h3>
<div class="mt-2">
<p class="text-sm text-gray-500 dark:text-gray-400">
{t("Give this token a name and select the organization it will have access to.")}
{t(
"Give this token a name and select the organization it will have access to."
)}
</p>
</div>
<div class="mt-4 space-y-4">
@ -1548,7 +1574,9 @@
</h3>
<div class="mt-2">
<p class="text-sm text-gray-500 dark:text-gray-400">
{t("Scan this QR code with your authenticator app (Google Authenticator, Authy, 1Password, etc.).")}
{t(
"Scan this QR code with your authenticator app (Google Authenticator, Authy, 1Password, etc.)."
)}
</p>
</div>
@ -1687,8 +1715,12 @@
</h3>
<div class="mt-2 text-sm text-amber-700 dark:text-amber-300">
<p>
{t("These codes won't be shown again. Store them securely (password manager, encrypted file, etc.).")}
{t("If you lose your authenticator and these codes, you won't be able to access your account.")}
{t(
"These codes won't be shown again. Store them securely (password manager, encrypted file, etc.)."
)}
{t(
"If you lose your authenticator and these codes, you won't be able to access your account."
)}
</p>
</div>
</div>

View file

@ -10,9 +10,10 @@ defmodule ToweropsWeb.Plugs.UpdateSessionActivity do
each session was last active. The update happens in a background task to avoid
slowing down requests.
"""
use Gettext, backend: ToweropsWeb.Gettext
import Phoenix.Controller, only: [put_flash: 3, redirect: 2]
import Plug.Conn
use Gettext, backend: ToweropsWeb.Gettext
alias Towerops.Accounts

View file

@ -14,34 +14,34 @@ msgstr ""
#: lib/towerops_web/live/admin/org_live/index.ex:33
#, elixir-autogen, elixir-format
msgid "Failed to delete organization"
msgstr ""
msgstr "No se pudo eliminar la organización"
#: lib/towerops_web/live/admin/user_live/index.ex:57
#, elixir-autogen, elixir-format
msgid "Failed to delete user"
msgstr ""
msgstr "No se pudo eliminar el usuario"
#: lib/towerops_web/user_auth.ex:859
#, elixir-autogen, elixir-format
msgid "Now impersonating %{email}"
msgstr ""
msgstr "Ahora estás suplantando a %{email}"
#: lib/towerops_web/live/admin/org_live/index.ex:29
#, elixir-autogen, elixir-format
msgid "Organization deleted successfully"
msgstr ""
msgstr "Organización eliminada correctamente"
#: lib/towerops_web/user_auth.ex:892
#, elixir-autogen, elixir-format
msgid "Stopped impersonating"
msgstr ""
msgstr "Suplantación finalizada"
#: lib/towerops_web/live/admin/user_live/index.ex:53
#, elixir-autogen, elixir-format
msgid "User deleted successfully"
msgstr ""
msgstr "Usuario eliminado correctamente"
#: lib/towerops_web/user_auth.ex:837
#, elixir-autogen, elixir-format
msgid "You cannot impersonate yourself."
msgstr ""
msgstr "No puedes suplantarte a ti mismo."

View file

@ -214,27 +214,27 @@ msgstr "✓ Alertas y notificaciones en tiempo real"
#: lib/towerops_web/live/user_settings_live/session_manager.ex:35
#, elixir-autogen, elixir-format
msgid "Alerts disabled for device"
msgstr ""
msgstr "Alertas desactivadas para el dispositivo"
#: lib/towerops_web/live/user_settings_live/session_manager.ex:35
#, elixir-autogen, elixir-format
msgid "Alerts enabled for device"
msgstr ""
msgstr "Alertas activadas para el dispositivo"
#: lib/towerops_web/live/user_settings_live/session_manager.ex:23
#, elixir-autogen, elixir-format
msgid "Failed to remove mobile device."
msgstr ""
msgstr "No se pudo eliminar el dispositivo móvil."
#: lib/towerops_web/live/user_settings_live/session_manager.ex:42
#, elixir-autogen, elixir-format
msgid "Failed to update alert preferences."
msgstr ""
msgstr "No se pudo actualizar las preferencias de alertas."
#: lib/towerops_web/live/user_settings_live/session_manager.ex:19
#, elixir-autogen, elixir-format
msgid "Mobile device removed successfully."
msgstr ""
msgstr "Dispositivo móvil eliminado correctamente."
#: lib/towerops_web/user_auth.ex:440
#: lib/towerops_web/user_auth.ex:447
@ -292,27 +292,27 @@ msgstr "Debes configurar la autenticación de dos factores para continuar."
#: lib/towerops_web/live/user_settings_live/api_token_manager.ex:66
#, elixir-autogen, elixir-format
msgid "API token deleted successfully."
msgstr ""
msgstr "Token de API eliminado correctamente."
#: lib/towerops_web/live/user_settings_live/totp_manager.ex:109
#, elixir-autogen, elixir-format
msgid "Cannot remove last device. You must have at least one."
msgstr ""
msgstr "No puedes eliminar el último dispositivo. Debes tener al menos uno."
#: lib/towerops_web/live/user_settings_live/totp_manager.ex:64
#, elixir-autogen, elixir-format
msgid "Device added successfully!"
msgstr ""
msgstr "¡Dispositivo agregado correctamente!"
#: lib/towerops_web/live/user_settings_live/totp_manager.ex:102
#, elixir-autogen, elixir-format
msgid "Device removed successfully."
msgstr ""
msgstr "Dispositivo eliminado correctamente."
#: lib/towerops_web/live/user_settings_live/api_token_manager.ex:39
#, elixir-autogen, elixir-format, fuzzy
#, elixir-autogen, elixir-format
msgid "Failed to create API token."
msgstr "No se pudo crear el dispositivo."
msgstr "No se pudo crear el token de API."
#: lib/towerops_web/live/user_settings_live/totp_manager.ex:44
#, elixir-autogen, elixir-format
@ -322,22 +322,22 @@ msgstr "No se pudo crear el dispositivo."
#: lib/towerops_web/live/user_settings_live/api_token_manager.ex:70
#, elixir-autogen, elixir-format
msgid "Failed to delete API token."
msgstr ""
msgstr "No se pudo eliminar el token de API."
#: lib/towerops_web/live/user_settings_live/totp_manager.ex:131
#, elixir-autogen, elixir-format, fuzzy
#, elixir-autogen, elixir-format
msgid "Failed to generate recovery codes."
msgstr "No se pudo crear el dispositivo."
msgstr "No se pudieron generar los códigos de recuperación."
#: lib/towerops_web/live/user_settings_live/totp_manager.ex:113
#, elixir-autogen, elixir-format, fuzzy
#, elixir-autogen, elixir-format
msgid "Failed to remove device."
msgstr "No se pudo crear el dispositivo."
msgstr "No se pudo eliminar el dispositivo."
#: lib/towerops_web/live/user_settings_live/session_manager.ex:66
#, elixir-autogen, elixir-format
msgid "Failed to revoke session."
msgstr ""
msgstr "No se pudo revocar la sesión."
#: lib/towerops_web/live/user_settings_live/totp_manager.ex:71
#, elixir-autogen, elixir-format
@ -347,32 +347,32 @@ msgstr "Código inválido. Inténtalo de nuevo."
#: lib/towerops_web/live/user_settings_live/api_token_manager.ex:42
#, elixir-autogen, elixir-format
msgid "Name and organization are required."
msgstr ""
msgstr "El nombre y la organización son obligatorios."
#: lib/towerops_web/live/user_settings_live/session_manager.ex:94
#, elixir-autogen, elixir-format
msgid "Revoked %{count} other session(s) successfully."
msgstr ""
msgstr "%{count} otra(s) sesión(es) revocada(s) correctamente."
#: lib/towerops_web/live/user_settings_live/session_manager.ex:63
#, elixir-autogen, elixir-format
msgid "Session not found."
msgstr ""
msgstr "Sesión no encontrada."
#: lib/towerops_web/live/user_settings_live/session_manager.ex:56
#, elixir-autogen, elixir-format
msgid "Session revoked successfully."
msgstr ""
msgstr "Sesión revocada correctamente."
#: lib/towerops_web/live/user_settings_live/api_token_manager.ex:73
#, elixir-autogen, elixir-format
msgid "You can only delete your own tokens."
msgstr ""
msgstr "Solo puedes eliminar tus propios tokens."
#: lib/towerops_web/live/user_settings_live/session_manager.ex:60
#, elixir-autogen, elixir-format
msgid "You cannot revoke your current session."
msgstr ""
msgstr "No puedes revocar tu sesión actual."
#: lib/towerops_web/controllers/user_settings_html/edit.html.heex:4
#, elixir-autogen, elixir-format
@ -536,9 +536,9 @@ msgid "Tip: If you prefer passwords, you can enable them in the user settings."
msgstr "Consejo: Si prefieres usar contraseñas, puedes habilitarlas en la configuración de tu cuenta."
#: lib/towerops_web/controllers/user_session_html/new.html.heex:37
#, elixir-autogen, elixir-format, fuzzy
#, elixir-autogen, elixir-format
msgid "To see sent emails, visit"
msgstr "Para ver los correos enviados, visita %{link}."
msgstr "Para ver los correos enviados, visita"
#: lib/towerops_web/controllers/user_session_html/totp.html.heex:5
#, elixir-autogen, elixir-format

File diff suppressed because it is too large Load diff

View file

@ -210,111 +210,111 @@ msgid "You don't have access to this alert"
msgstr "No tienes acceso a esta alerta"
#: lib/towerops_web/live/agent_live/index.ex:339
#, elixir-autogen, elixir-format, fuzzy
#, elixir-autogen, elixir-format
msgid "Agent created successfully"
msgstr "Dispositivo vinculado correctamente"
msgstr "Agente creado correctamente"
#: lib/towerops_web/live/agent_live/index.ex:215
#, elixir-autogen, elixir-format
msgid "Agent deleted successfully. Devices now fall back to site/org defaults or cloud polling."
msgstr ""
msgstr "Agente eliminado correctamente. Los dispositivos ahora usan los valores predeterminados del sitio/organización o sondeo en la nube."
#: lib/towerops_web/live/agent_live/edit.ex:55
#, elixir-autogen, elixir-format, fuzzy
#, elixir-autogen, elixir-format
msgid "Agent updated successfully"
msgstr "Sitio actualizado correctamente"
msgstr "Agente actualizado correctamente"
#: lib/towerops_web/live/agent_live/index.ex:338
#, elixir-autogen, elixir-format
msgid "Cloud poller created successfully"
msgstr ""
msgstr "Cloud poller creado correctamente"
#: lib/towerops_web/live/agent_live/index.ex:106
#, elixir-autogen, elixir-format
msgid "Failed to create agent"
msgstr ""
msgstr "No se pudo crear el agente"
#: lib/towerops_web/live/agent_live/index.ex:105
#, elixir-autogen, elixir-format, fuzzy
#, elixir-autogen, elixir-format
msgid "Failed to create cloud poller"
msgstr "No se pudo actualizar el rol"
msgstr "No se pudo crear el cloud poller"
#: lib/towerops_web/live/agent_live/index.ex:219
#, elixir-autogen, elixir-format, fuzzy
#, elixir-autogen, elixir-format
msgid "Failed to delete agent"
msgstr "No se pudo actualizar el rol"
msgstr "No se pudo eliminar el agente"
#: lib/towerops_web/live/agent_live/index.ex:151
#, elixir-autogen, elixir-format, fuzzy
#, elixir-autogen, elixir-format
msgid "Failed to generate new token"
msgstr "No se pudo generar la diferencia: %{reason}"
msgstr "No se pudo generar el nuevo token"
#: lib/towerops_web/live/agent_live/index.ex:155
#, elixir-autogen, elixir-format, fuzzy
#, elixir-autogen, elixir-format
msgid "Failed to revoke old token"
msgstr "No se pudo reordenar el sitio"
msgstr "No se pudo revocar el token anterior"
#: lib/towerops_web/live/agent_live/index.ex:248
#, elixir-autogen, elixir-format
msgid "Failed to update global default cloud poller"
msgstr ""
msgstr "No se pudo actualizar el cloud poller predeterminado global"
#: lib/towerops_web/live/agent_live/index.ex:367
#, elixir-autogen, elixir-format
msgid "Global default cloud poller cleared"
msgstr ""
msgstr "Cloud poller predeterminado global eliminado"
#: lib/towerops_web/live/agent_live/index.ex:366
#, elixir-autogen, elixir-format
msgid "Global default cloud poller set successfully"
msgstr ""
msgstr "Cloud poller predeterminado global configurado correctamente"
#: lib/towerops_web/live/agent_live/index.ex:148
#, elixir-autogen, elixir-format
msgid "New token generated successfully"
msgstr ""
msgstr "Nuevo token generado correctamente"
#: lib/towerops_web/live/agent_live/index.ex:100
#, elixir-autogen, elixir-format
msgid "Only superadmins can create cloud pollers"
msgstr ""
msgstr "Solo los superadministradores pueden crear cloud pollers"
#: lib/towerops_web/live/agent_live/index.ex:251
#, elixir-autogen, elixir-format
msgid "Only superadmins can set the global default cloud poller"
msgstr ""
msgstr "Solo los superadministradores pueden configurar el cloud poller predeterminado global"
#: lib/towerops_web/live/agent_live/index.ex:245
#, elixir-autogen, elixir-format
msgid "Selected agent no longer exists. Please choose another agent."
msgstr ""
msgstr "El agente seleccionado ya no existe. Elige otro agente."
#: lib/towerops_web/live/alert_live/index.ex:71
#, elixir-autogen, elixir-format
msgid "Alert resolved"
msgstr ""
msgstr "Alerta resuelta"
#: lib/towerops_web/live/device_live/show.ex:1478
#, elixir-autogen, elixir-format, fuzzy
#, elixir-autogen, elixir-format
msgid "Backup deleted successfully"
msgstr "Dispositivo actualizado correctamente"
msgstr "Respaldo eliminado correctamente"
#: lib/towerops_web/live/device_live/show.ex:1482
#, elixir-autogen, elixir-format, fuzzy
#, elixir-autogen, elixir-format
msgid "Failed to delete backup"
msgstr "No se pudo actualizar el rol"
msgstr "No se pudo eliminar el respaldo"
#: lib/towerops_web/live/device_live/show.ex:1367
#, elixir-autogen, elixir-format
msgid "Only organization owners can delete backups"
msgstr ""
msgstr "Solo los propietarios de la organización pueden eliminar respaldos"
#: lib/towerops_web/live/device_live/show.ex:1340
#, elixir-autogen, elixir-format
msgid "Please select exactly 2 backups to compare"
msgstr ""
msgstr "Selecciona exactamente 2 respaldos para comparar"
#: lib/towerops_web/live/alert_live/index.ex:75
#, elixir-autogen, elixir-format, fuzzy
#, elixir-autogen, elixir-format
msgid "Unable to resolve alert"
msgstr "No se pudo reconocer la alerta"
msgstr "No se pudo resolver la alerta"