i18n: wrap all user-facing strings in gettext()

This commit is contained in:
Graham McIntire 2026-02-14 17:31:03 -06:00
parent 6623f2e24a
commit 243b773e39
92 changed files with 1200 additions and 1196 deletions

View file

@ -43,6 +43,7 @@ defmodule ToweropsWeb do
use Gettext, backend: ToweropsWeb.Gettext
import Plug.Conn
import ToweropsWeb.GettextHelpers
unquote(verified_routes())
end

View file

@ -293,12 +293,12 @@ defmodule ToweropsWeb.Layouts do
class="inline-flex items-center justify-center gap-x-1 rounded-md bg-white px-2 py-1.5 sm:px-3 sm:py-2 text-xs sm:text-sm font-semibold text-gray-900 shadow-xs inset-ring-1 inset-ring-gray-300 hover:bg-gray-50 dark:bg-white/10 dark:text-white dark:shadow-none dark:inset-ring-white/5 dark:hover:bg-white/20"
>
<span class="hidden sm:inline">
{if @current_organization, do: @current_organization.name, else: "Menu"}
{if @current_organization, do: @current_organization.name, else: t("Menu")}
</span>
<span class="sm:hidden">
{if @current_organization,
do: String.slice(@current_organization.name, 0..10),
else: "Menu"}
else: t("Menu")}
</span>
<svg
viewBox="0 0 20 20"

View file

@ -74,7 +74,7 @@
href="#main-content"
class="sr-only focus:not-sr-only focus:absolute focus:top-0 focus:left-0 focus:z-50 focus:p-4 focus:bg-blue-600 focus:text-white focus:rounded-md focus:m-2"
>
Skip to main content
{t("Skip to main content")}
</a>
{@inner_content}
</body>

View file

@ -3,7 +3,7 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>404 - Page Not Found</title>
<title>{t("404 - Page Not Found")}</title>
<link phx-track-static rel="stylesheet" href="/assets/css/app.css" />
</head>
<body class="bg-white">
@ -19,17 +19,17 @@
<div class="mb-8">
<img
src="/images/squirrel.jpg"
alt="Squirrel eating wires"
alt={t("Squirrel eating wires")}
class="rounded-2xl shadow-xl mx-auto max-w-md w-full"
/>
</div>
<!-- Error Message -->
<h2 class="text-3xl font-bold text-slate-900 mb-4">
Oops, a squirrel must have eaten that wire!
{t("Oops, a squirrel must have eaten that wire!")}
</h2>
<p class="text-lg text-slate-600 mb-8">
The page you're looking for could not be found.
{t("The page you're looking for could not be found.")}
</p>
<!-- Back Home Button -->
@ -37,7 +37,7 @@
href="/"
class="inline-flex items-center justify-center rounded-full py-3 px-6 text-sm font-semibold bg-blue-600 text-white hover:bg-blue-500 active:bg-blue-800 transition-colors"
>
Go back home
{t("Go back home")}
</a>
</div>
</div>

View file

@ -3,7 +3,7 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>500 - Server Error</title>
<title>{t("500 - Server Error")}</title>
<link phx-track-static rel="stylesheet" href="/assets/css/app.css" />
</head>
<body class="bg-white">
@ -17,10 +17,10 @@
<!-- Error Message -->
<h2 class="text-3xl font-bold text-slate-900 mb-4">
Something went wrong on our end
{t("Something went wrong on our end")}
</h2>
<p class="text-lg text-slate-600 mb-8">
We're working on fixing the problem. Please try again later.
{t("We're working on fixing the problem. Please try again later.")}
</p>
<!-- Back Home Button -->
@ -28,7 +28,7 @@
href="/"
class="inline-flex items-center justify-center rounded-full py-3 px-6 text-sm font-semibold bg-blue-600 text-white hover:bg-blue-500 active:bg-blue-800 transition-colors"
>
Go back home
{t("Go back home")}
</a>
</div>
</div>

View file

@ -7,7 +7,7 @@ defmodule ToweropsWeb.InvitationController do
case Organizations.get_invitation_by_token(token) do
nil ->
conn
|> put_flash(:error, "This invitation is invalid or has expired.")
|> put_flash(:error, t("This invitation is invalid or has expired."))
|> redirect(to: ~p"/")
invitation ->
@ -29,12 +29,12 @@ defmodule ToweropsWeb.InvitationController do
case Organizations.accept_invitation(invitation, user.id) do
{:ok, _membership} ->
conn
|> put_flash(:info, "You've joined #{invitation.organization.name}!")
|> put_flash(:info, t("You've joined %{org}!", org: invitation.organization.name))
|> redirect(to: ~p"/orgs/#{invitation.organization.slug}/settings?tab=members")
{:error, _changeset} ->
conn
|> put_flash(:error, "Could not accept invitation. You may already be a member.")
|> put_flash(:error, t("Could not accept invitation. You may already be a member."))
|> redirect(to: ~p"/dashboard")
end
end

View file

@ -30,12 +30,12 @@ defmodule ToweropsWeb.UserConfirmationController do
case Accounts.confirm_user(token) do
{:ok, _user} ->
conn
|> put_flash(:info, "Email confirmed successfully. You may now log in.")
|> put_flash(:info, t("Email confirmed successfully. You may now log in."))
|> redirect(to: ~p"/users/log-in")
:error ->
conn
|> put_flash(:error, "Email confirmation link is invalid or has expired.")
|> put_flash(:error, t("Email confirmation link is invalid or has expired."))
|> redirect(to: ~p"/users/log-in")
end
end

View file

@ -1,15 +1,15 @@
<Layouts.app flash={@flash} current_scope={@current_scope}>
<div class="mx-auto max-w-sm">
<.header>Resend confirmation instructions</.header>
<.header>{t_auth("Resend confirmation instructions")}</.header>
<.form for={%{}} as={:user} action={~p"/users/confirm"} class="mt-8">
<.input name="user[email]" type="email" label="Email" required />
<.button class="w-full mt-4">Resend confirmation instructions</.button>
<.input name="user[email]" type="email" label={t_auth("Email")} required />
<.button class="w-full mt-4">{t_auth("Resend confirmation instructions")}</.button>
</.form>
<p class="text-center text-sm mt-4">
<.link href={~p"/users/log-in"}>Log in</.link>
| <.link href={~p"/users/register"}>Register</.link>
<.link href={~p"/users/log-in"}>{t_auth("Log in")}</.link>
| <.link href={~p"/users/register"}>{t_auth("Register")}</.link>
</p>
</div>
</Layouts.app>

View file

@ -38,7 +38,7 @@ defmodule ToweropsWeb.UserRegistrationController do
case Accounts.register_user_with_organization(user_params) do
{:ok, user} ->
conn
|> put_flash(:info, "Account created successfully.")
|> put_flash(:info, t("Account created successfully."))
|> UserAuth.log_in_user(user)
{:error, %Ecto.Changeset{} = changeset} ->
@ -55,14 +55,14 @@ defmodule ToweropsWeb.UserRegistrationController do
{:ok, user} <- Accounts.register_user(user_params),
{:ok, _membership} <- Organizations.accept_invitation(invitation, user.id) do
conn
|> put_flash(:info, "Account created successfully. Welcome to #{invitation.organization.name}!")
|> put_flash(:info, t("Account created successfully. Welcome to %{org}!", org: invitation.organization.name))
|> UserAuth.log_in_user(user)
else
{:error, :invalid_invitation} ->
changeset = Accounts.change_user_registration(%User{}, user_params)
conn
|> put_flash(:error, "Invalid or expired invitation link.")
|> put_flash(:error, t("Invalid or expired invitation link."))
|> render(:new,
form: Phoenix.Component.to_form(changeset, as: "user"),
invitation: nil,

View file

@ -33,7 +33,7 @@ defmodule ToweropsWeb.UserResetPasswordController do
case Accounts.get_user_by_reset_password_token(token) do
nil ->
conn
|> put_flash(:error, "Reset password link is invalid or has expired.")
|> put_flash(:error, t("Reset password link is invalid or has expired."))
|> redirect(to: ~p"/users/log-in")
_user ->
@ -46,14 +46,14 @@ defmodule ToweropsWeb.UserResetPasswordController do
case Accounts.get_user_by_reset_password_token(token) do
nil ->
conn
|> put_flash(:error, "Reset password link is invalid or has expired.")
|> put_flash(:error, t("Reset password link is invalid or has expired."))
|> redirect(to: ~p"/users/log-in")
user ->
case Accounts.reset_user_password(user, user_params) do
{:ok, {updated_user, _expired_tokens}} ->
conn
|> put_flash(:info, "Password reset successfully.")
|> put_flash(:info, t("Password reset successfully."))
|> UserAuth.log_in_user(updated_user, %{"remember_me" => "true"})
{:error, changeset} ->

View file

@ -20,7 +20,7 @@ defmodule ToweropsWeb.UserSessionController do
{:ok, {user, _expired_tokens}} ->
conn =
if params["_action"] == "confirmed" do
put_flash(conn, :info, "User confirmed successfully.")
put_flash(conn, :info, t("User confirmed successfully."))
else
conn
end
@ -29,7 +29,7 @@ defmodule ToweropsWeb.UserSessionController do
{:error, :not_found} ->
conn
|> put_flash(:error, "The link is invalid or it has expired.")
|> put_flash(:error, t("The link is invalid or it has expired."))
|> render(:new, form: Phoenix.Component.to_form(%{}, as: "user"))
end
end
@ -64,7 +64,7 @@ defmodule ToweropsWeb.UserSessionController do
# In order to prevent user enumeration attacks, don't disclose whether the email is registered.
conn
|> put_flash(:error, "Invalid email or password")
|> put_flash(:error, t("Invalid email or password"))
|> render(:new, form: form)
end
end
@ -101,7 +101,7 @@ defmodule ToweropsWeb.UserSessionController do
if was_confirmed do
conn
else
put_flash(conn, :info, "User confirmed successfully.")
put_flash(conn, :info, t("User confirmed successfully."))
end
# Always use remember_me: true to keep user logged in
@ -109,14 +109,14 @@ defmodule ToweropsWeb.UserSessionController do
{:error, :not_found} ->
conn
|> put_flash(:error, "Magic link is invalid or it has expired.")
|> put_flash(:error, t("Magic link is invalid or it has expired."))
|> redirect(to: ~p"/users/log-in")
end
end
def delete(conn, _params) do
conn
|> put_flash(:info, "Logged out successfully.")
|> put_flash(:info, t("Logged out successfully."))
|> UserAuth.log_out_user()
end
@ -124,7 +124,7 @@ defmodule ToweropsWeb.UserSessionController do
case get_session(conn, :pending_totp_user_id) do
nil ->
conn
|> put_flash(:error, "Please log in first.")
|> put_flash(:error, t("Please log in first."))
|> redirect(to: ~p"/users/log-in")
_user_id ->
@ -145,7 +145,7 @@ defmodule ToweropsWeb.UserSessionController do
defp redirect_to_login(conn) do
conn
|> put_flash(:error, "Please log in first.")
|> put_flash(:error, t("Please log in first."))
|> redirect(to: ~p"/users/log-in")
end
@ -189,7 +189,7 @@ defmodule ToweropsWeb.UserSessionController do
form = Phoenix.Component.to_form(%{"totp_code" => code}, as: "user")
conn
|> put_flash(:error, "Invalid authentication code. Please try again.")
|> put_flash(:error, t("Invalid authentication code. Please try again."))
|> render(:totp, form: form)
end
end

View file

@ -1,7 +1,7 @@
<Layouts.app flash={@flash} current_scope={@current_scope}>
<div class="mx-auto max-w-sm">
<div class="text-center">
<.header>Welcome {@user.email}</.header>
<.header>{t_auth("Welcome %{email}", email: @user.email)}</.header>
</div>
<div class="mt-8 space-y-3">
@ -17,14 +17,14 @@
<.button
name={@form[:remember_me].name}
value="true"
phx-disable-with="Confirming..."
phx-disable-with={t_auth("Confirming...")}
class="w-full"
variant="primary"
>
Confirm and stay logged in
{t_auth("Confirm and stay logged in")}
</.button>
<.button phx-disable-with="Confirming..." class="w-full">
Confirm and log in only this time
<.button phx-disable-with={t_auth("Confirming...")} class="w-full">
{t_auth("Confirm and log in only this time")}
</.button>
</div>
</.form>
@ -38,22 +38,22 @@
>
<input type="hidden" name={@form[:token].name} value={@form[:token].value} />
<%= if @current_scope do %>
<.button variant="primary" phx-disable-with="Logging in..." class="w-full">
Log in
<.button variant="primary" phx-disable-with={t_auth("Logging in...")} class="w-full">
{t_auth("Log in")}
</.button>
<% else %>
<div class="space-y-3">
<.button
name={@form[:remember_me].name}
value="true"
phx-disable-with="Logging in..."
phx-disable-with={t_auth("Logging in...")}
class="w-full"
variant="primary"
>
Log in and stay logged in
{t_auth("Log in and stay logged in")}
</.button>
<.button phx-disable-with="Logging in..." class="w-full">
Log in only this time
<.button phx-disable-with={t_auth("Logging in...")} class="w-full">
{t_auth("Log in only this time")}
</.button>
</div>
<% end %>
@ -64,7 +64,7 @@
:if={!@user.confirmed_at}
class="mt-6 rounded-lg border border-blue-200 bg-blue-50 p-4 text-sm text-blue-700 dark:border-blue-800 dark:bg-blue-950 dark:text-blue-200"
>
Tip: If you prefer passwords, you can enable them in the user settings.
{t_auth("Tip: If you prefer passwords, you can enable them in the user settings.")}
</p>
</div>
</Layouts.app>

View file

@ -111,7 +111,7 @@
href={~p"/users/confirm"}
class="text-blue-600 hover:text-blue-700 dark:text-blue-400"
>
Didn't receive confirmation instructions?
{t_auth("Didn't receive confirmation instructions?")}
</.link>
</p>
</div>

View file

@ -2,9 +2,9 @@
<div class="mx-auto max-w-sm">
<div class="text-center">
<.header>
Two-Factor Authentication
{t_auth("Two-Factor Authentication")}
<:subtitle>
Enter the 6-digit code from your authenticator app
{t_auth("Enter the 6-digit code from your authenticator app")}
</:subtitle>
</.header>
</div>
@ -14,7 +14,7 @@
<.input
field={f[:totp_code]}
type="text"
label="Authentication Code"
label={t_auth("Authentication Code")}
placeholder="000000"
autocomplete="one-time-code"
inputmode="numeric"
@ -23,12 +23,12 @@
/>
<div class="mt-2">
<p class="text-sm text-gray-500 dark:text-gray-400">
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">
<.button class="w-full" variant="primary">
Verify <span aria-hidden="true">→</span>
{t_auth("Verify")} <span aria-hidden="true">→</span>
</.button>
</div>
</.form>
@ -38,7 +38,7 @@
href={~p"/users/log-in"}
class="text-sm font-semibold text-gray-600 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300"
>
← Back to login
{t_auth("← Back to login")}
</.link>
</div>
</div>

View file

@ -13,12 +13,12 @@ defmodule ToweropsWeb.UserSettingsController do
case Accounts.update_user_email(conn.assigns.current_scope.user, token) do
{:ok, _user} ->
conn
|> put_flash(:info, "Email changed successfully.")
|> put_flash(:info, t("Email changed successfully."))
|> redirect(to: ~p"/users/settings")
{:error, _} ->
conn
|> put_flash(:error, "Email change link is invalid or it has expired.")
|> put_flash(:error, t("Email change link is invalid or it has expired."))
|> redirect(to: ~p"/users/settings")
end
end

View file

@ -1,17 +1,17 @@
<Layouts.authenticated flash={@flash} current_scope={@current_scope}>
<div class="text-center">
<.header>
Account Settings
<:subtitle>Manage your account email address and password settings</:subtitle>
{t_auth("Account Settings")}
<:subtitle>{t_auth("Manage your account email address and password settings")}</:subtitle>
</.header>
</div>
<.form :let={f} for={@email_changeset} action={~p"/users/settings"} id="update_email">
<input type="hidden" name="action" value="update_email" />
<.input field={f[:email]} type="email" label="Email" autocomplete="email" required />
<.input field={f[:email]} type="email" label={t_auth("Email")} autocomplete="email" required />
<.button variant="primary" phx-disable-with="Changing...">Change Email</.button>
<.button variant="primary" phx-disable-with={t_auth("Changing...")}>{t_auth("Change Email")}</.button>
</.form>
<div class="divider" />
@ -22,19 +22,19 @@
<.input
field={f[:password]}
type="password"
label="New password"
label={t_auth("New password")}
autocomplete="new-password"
required
/>
<.input
field={f[:password_confirmation]}
type="password"
label="Confirm new password"
label={t_auth("Confirm new password")}
autocomplete="new-password"
required
/>
<.button variant="primary" phx-disable-with="Changing...">
Save Password
<.button variant="primary" phx-disable-with={t_auth("Changing...")}>
{t_auth("Save Password")}
</.button>
</.form>
@ -42,10 +42,10 @@
<div>
<h3 class="text-lg font-semibold leading-6 text-gray-900 dark:text-white">
Alert Notification Devices
{t_auth("Alert Notification Devices")}
</h3>
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400">
Manage mobile devices that receive push notifications for alerts
{t_auth("Manage mobile devices that receive push notifications for alerts")}
</p>
<div class="mt-6 space-y-4">
@ -53,10 +53,10 @@
<div class="rounded-lg border border-dashed border-gray-300 p-8 text-center dark:border-white/10">
<.icon name="hero-device-phone-mobile" class="mx-auto h-12 w-12 text-gray-400" />
<h3 class="mt-2 text-sm font-semibold text-gray-900 dark:text-white">
No mobile devices registered
{t_auth("No mobile devices registered")}
</h3>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
Add a mobile device to receive push notifications for alerts
{t_auth("Add a mobile device to receive push notifications for alerts")}
</p>
</div>
<% else %>
@ -67,13 +67,13 @@
<.icon name="hero-device-phone-mobile" class="h-5 w-5 text-gray-400" />
<div>
<p class="font-medium text-gray-900 dark:text-white">
{session.device_name || "Unknown Device"}
{session.device_name || t_auth("Unknown Device")}
</p>
<p class="text-sm text-gray-500 dark:text-gray-400">
{session.device_os} • {session.app_version}
</p>
<p class="text-sm text-gray-500 dark:text-gray-400">
Last used {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>
@ -105,9 +105,9 @@
]}
>
<%= if session.alerts_enabled do %>
<.icon name="hero-bell" class="h-4 w-4" /> Alerts On
<.icon name="hero-bell" class="h-4 w-4" /> {t_auth("Alerts On")}
<% else %>
<.icon name="hero-bell-slash" class="h-4 w-4" /> Alerts Off
<.icon name="hero-bell-slash" class="h-4 w-4" /> {t_auth("Alerts Off")}
<% end %>
</button>
</.form>
@ -122,10 +122,10 @@
<input type="hidden" name="session_id" value={session.id} />
<button
type="submit"
data-confirm="Are you sure you want to remove this device?"
data-confirm={t_auth("Are you sure you want to remove this device?")}
class="text-sm font-semibold text-red-600 hover:text-red-500 dark:text-red-400 dark:hover:text-red-300"
>
Remove
{t_auth("Remove")}
</button>
</.form>
</div>
@ -138,7 +138,7 @@
navigate={~p"/mobile/qr-login"}
class="inline-flex items-center gap-2 rounded-lg bg-gray-900 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-gray-700 dark:bg-gray-50 dark:text-gray-900 dark:hover:bg-gray-300"
>
<.icon name="hero-qr-code" class="h-4 w-4" /> Add Mobile Device
<.icon name="hero-qr-code" class="h-4 w-4" /> {t_auth("Add Mobile Device")}
</.link>
</div>
</div>

View file

@ -29,7 +29,7 @@ defmodule ToweropsWeb.UserSudoController do
# No TOTP devices, must enroll first
!Accounts.totp_enabled?(user) ->
conn
|> put_flash(:error, "Two-factor authentication is required for this action.")
|> put_flash(:error, t("Two-factor authentication is required for this action."))
|> redirect(to: ~p"/account/totp-enrollment")
# Show verification form
@ -61,7 +61,7 @@ defmodule ToweropsWeb.UserSudoController do
return_to = get_session(conn, :user_return_to) || ~p"/users/settings"
conn
|> put_flash(:info, "Identity verified.")
|> put_flash(:info, t("Identity verified."))
|> delete_session(:user_return_to)
|> redirect(to: return_to)
@ -70,7 +70,7 @@ defmodule ToweropsWeb.UserSudoController do
form = Phoenix.Component.to_form(%{"totp_code" => totp_code}, as: "user")
conn
|> put_flash(:error, "Failed to grant sudo mode. Please try again.")
|> put_flash(:error, t("Failed to grant sudo mode. Please try again."))
|> render(:verify, form: form)
end
@ -88,7 +88,7 @@ defmodule ToweropsWeb.UserSudoController do
form = Phoenix.Component.to_form(%{"totp_code" => totp_code}, as: "user")
conn
|> put_flash(:error, "Invalid authentication code. Please try again.")
|> put_flash(:error, t("Invalid authentication code. Please try again."))
|> render(:verify, form: form)
end
end

View file

@ -2,9 +2,9 @@
<div class="mx-auto max-w-sm">
<div class="text-center">
<.header>
Re-authenticate
{t_auth("Re-authenticate")}
<:subtitle>
Enter the 6-digit code from your authenticator app to continue
{t_auth("Enter the 6-digit code from your authenticator app to continue")}
</:subtitle>
</.header>
</div>
@ -14,7 +14,7 @@
<.input
field={f[:totp_code]}
type="text"
label="Authentication Code"
label={t_auth("Authentication Code")}
placeholder="000000"
autocomplete="one-time-code"
inputmode="numeric"
@ -23,12 +23,12 @@
/>
<div class="mt-2">
<p class="text-sm text-gray-500 dark:text-gray-400">
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">
<.button class="w-full" variant="primary">
Verify <span aria-hidden="true">→</span>
{t_auth("Verify")} <span aria-hidden="true">→</span>
</.button>
</div>
</.form>
@ -39,7 +39,7 @@
method="delete"
class="text-sm font-semibold text-gray-600 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300"
>
← Log out instead
{t_auth("← Log out instead")}
</.link>
</div>
</div>

View file

@ -16,7 +16,7 @@ defmodule ToweropsWeb.AccountLive.Activity do
{:ok,
socket
|> assign(:page_title, "Activity Log")
|> assign(:page_title, t("Activity Log"))
|> assign(:audit_logs, audit_logs)}
end

View file

@ -19,13 +19,13 @@ defmodule ToweropsWeb.AccountLive.MyData do
socket =
socket
|> put_flash(:info, "Consent revoked successfully.")
|> put_flash(:info, t("Consent revoked successfully."))
|> assign(:user_data, Map.put(socket.assigns.user_data, :consents, get_user_consents(user)))
{:noreply, socket}
{:error, _} ->
{:noreply, put_flash(socket, :error, "Failed to revoke consent.")}
{:noreply, put_flash(socket, :error, t("Failed to revoke consent."))}
end
end
@ -55,7 +55,7 @@ defmodule ToweropsWeb.AccountLive.MyData do
socket
|> assign(:current_scope, scope)
|> assign(:user_data, user_data)
|> assign(:page_title, "My Data")
|> assign(:page_title, t("My Data"))
|> assign(:is_owner, is_owner)}
end

View file

@ -42,7 +42,7 @@ defmodule ToweropsWeb.AccountLive.TotpEnrollment do
socket =
socket
|> assign(:page_title, "Set Up Two-Factor Authentication")
|> assign(:page_title, t("Set Up Two-Factor Authentication"))
|> assign(:secret_base32, secret_base32)
|> assign(:qr_code, qr_code)
|> assign(:code, "")
@ -57,7 +57,7 @@ defmodule ToweropsWeb.AccountLive.TotpEnrollment do
# Real secret will be generated when socket connects
socket =
socket
|> assign(:page_title, "Set Up Two-Factor Authentication")
|> assign(:page_title, t("Set Up Two-Factor Authentication"))
|> assign(:secret_base32, nil)
|> assign(:qr_code, nil)
|> assign(:code, "")
@ -94,7 +94,7 @@ defmodule ToweropsWeb.AccountLive.TotpEnrollment do
handle_valid_code(socket, user, secret)
else
Logger.warning("TOTP enrollment verification failed", user_email: user.email)
{:noreply, assign(socket, :error, "Invalid code. Please try again.")}
{:noreply, assign(socket, :error, t("Invalid code. Please try again."))}
end
end
@ -105,7 +105,7 @@ defmodule ToweropsWeb.AccountLive.TotpEnrollment do
{:noreply,
socket
|> put_flash(:info, "Two-factor authentication enabled successfully!")
|> put_flash(:info, t("Two-factor authentication enabled successfully!"))
|> redirect(to: redirect_path)}
end
@ -124,7 +124,7 @@ defmodule ToweropsWeb.AccountLive.TotpEnrollment do
|> assign(:show_recovery_codes, true)}
{:error, _changeset} ->
{:noreply, assign(socket, :error, "Failed to enable two-factor authentication. Please try again.")}
{:noreply, assign(socket, :error, t("Failed to enable two-factor authentication. Please try again."))}
end
end

View file

@ -30,7 +30,7 @@ defmodule ToweropsWeb.ActivityFeedLive do
{:ok,
socket
|> assign(:page_title, "Activity Feed")
|> assign(:page_title, t("Activity Feed"))
|> assign(:active_types, @all_types)
|> assign(:limit, @default_limit)
|> assign(:has_more, true)

View file

@ -10,10 +10,10 @@
</span>
<span class="relative inline-flex rounded-full h-3 w-3 bg-green-500"></span>
</span>
Activity Feed
{t("Activity Feed")}
</div>
<:subtitle>
Real-time NOC operations log — config changes, alerts, events, and syncs
{t("Real-time NOC operations log — config changes, alerts, events, and syncs")}
</:subtitle>
</.header>
@ -29,7 +29,7 @@
type="text"
name="search"
value={@search}
placeholder="Search by device name, IP, site, or keyword..."
placeholder={t("Search by device name, IP, site, or keyword...")}
phx-debounce="300"
class="input input-bordered w-full pl-10 pr-10 bg-white dark:bg-gray-800 dark:border-gray-700 dark:text-white dark:placeholder-gray-500"
/>
@ -91,14 +91,14 @@
<%= if @search != "" do %>
No results for "{@search}"
<% else %>
No activity yet
{t("No activity yet")}
<% end %>
</h3>
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400 max-w-md mx-auto">
<%= if @search != "" do %>
Try adjusting your search terms or clearing the filters.
{t("Try adjusting your search terms or clearing the filters.")}
<% else %>
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">
@ -126,7 +126,7 @@
</span>
<span class="relative inline-flex rounded-full h-2 w-2 bg-green-500"></span>
</span>
Live
{t("Live")}
</span>
</div>

View file

@ -20,7 +20,7 @@ defmodule ToweropsWeb.Admin.AgentLive.Index do
{:ok,
socket
|> assign(:page_title, "All Agents")
|> assign(:page_title, t("All Agents"))
|> assign(:timezone, socket.assigns.current_scope.timezone)
|> stream(:cloud_pollers, cloud_pollers)
|> stream(:org_agents, org_agents)

View file

@ -1,9 +1,9 @@
<Layouts.admin flash={@flash} timezone={@timezone}>
<div class="space-y-6">
<div>
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">All Agents</h1>
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">{t("All Agents")}</h1>
<p class="text-gray-600 dark:text-gray-400">
Agents across all organizations and cloud pollers
{t("Agents across all organizations and cloud pollers")}
</p>
</div>
@ -18,7 +18,7 @@
rows={@streams.cloud_pollers}
row_link={fn {_id, agent} -> ~p"/agents/#{agent.id}" end}
>
<:col :let={{_id, agent}} label="Name">
<:col :let={{_id, agent}} label={t("Name")}>
<div class="flex items-center gap-2">
<span class="font-medium">{agent.name}</span>
<%= if agent.allow_remote_debug do %>
@ -29,7 +29,7 @@
</div>
</:col>
<:col :let={{_id, agent}} label="Status">
<:col :let={{_id, agent}} label={t("Status")}>
<% {status, label} = agent_status(agent) %>
<span class={"inline-flex items-center gap-1.5 rounded-full px-2.5 py-0.5 text-xs font-medium #{status_badge_class(status)}"}>
<span class={"h-1.5 w-1.5 rounded-full #{status_dot_class(status)}"} />
@ -37,7 +37,7 @@
</span>
</:col>
<:col :let={{_id, agent}} label="Devices">
<:col :let={{_id, agent}} label={t("Devices")}>
<% counts = Map.get(@device_counts, agent.id, %{direct: 0, total: 0}) %>
<div class="text-sm text-gray-900 dark:text-white">
{counts.total} total
@ -50,7 +50,7 @@
</div>
</:col>
<:col :let={{_id, agent}} label="Last Seen">
<:col :let={{_id, agent}} label={t("Last Seen")}>
<div class="text-sm text-gray-900 dark:text-white">
<.timestamp datetime={agent.last_seen_at} timezone={@timezone} now={@now} />
</div>
@ -65,7 +65,7 @@
<% end %>
</:col>
<:col :let={{_id, agent}} label="Version">
<:col :let={{_id, agent}} label={t("Version")}>
<%= if agent.metadata["version"] do %>
<div class="text-sm text-gray-600 dark:text-gray-400 font-mono">
{format_agent_version(agent.metadata["version"])}
@ -87,7 +87,7 @@
rows={@streams.org_agents}
row_link={fn {_id, agent} -> ~p"/agents/#{agent.id}" end}
>
<:col :let={{_id, agent}} label="Name">
<:col :let={{_id, agent}} label={t("Name")}>
<div>
<div class="flex items-center gap-2">
<span class="font-medium">{agent.name}</span>
@ -103,7 +103,7 @@
</div>
</:col>
<:col :let={{_id, agent}} label="Status">
<:col :let={{_id, agent}} label={t("Status")}>
<% {status, label} = agent_status(agent) %>
<span class={"inline-flex items-center gap-1.5 rounded-full px-2.5 py-0.5 text-xs font-medium #{status_badge_class(status)}"}>
<span class={"h-1.5 w-1.5 rounded-full #{status_dot_class(status)}"} />
@ -111,7 +111,7 @@
</span>
</:col>
<:col :let={{_id, agent}} label="Devices">
<:col :let={{_id, agent}} label={t("Devices")}>
<% counts = Map.get(@device_counts, agent.id, %{direct: 0, total: 0}) %>
<div class="text-sm text-gray-900 dark:text-white">
{counts.total} total
@ -124,7 +124,7 @@
</div>
</:col>
<:col :let={{_id, agent}} label="Last Seen">
<:col :let={{_id, agent}} label={t("Last Seen")}>
<div class="text-sm text-gray-900 dark:text-white">
<.timestamp datetime={agent.last_seen_at} timezone={@timezone} now={@now} />
</div>
@ -139,7 +139,7 @@
<% end %>
</:col>
<:col :let={{_id, agent}} label="Version">
<:col :let={{_id, agent}} label={t("Version")}>
<%= if agent.metadata["version"] do %>
<div class="text-sm text-gray-600 dark:text-gray-400 font-mono">
{format_agent_version(agent.metadata["version"])}

View file

@ -13,7 +13,7 @@ defmodule ToweropsWeb.Admin.AuditLive.Index do
def mount(_params, _session, socket) do
{:ok,
socket
|> assign(:page_title, "Audit Logs")
|> assign(:page_title, t("Audit Logs"))
|> assign(:search_email, "")
|> assign(:search_action, "")
|> assign(:date_from, "")

View file

@ -10,7 +10,7 @@ defmodule ToweropsWeb.Admin.DashboardLive do
def mount(_params, _session, socket) do
{:ok,
socket
|> assign(:page_title, "Admin Dashboard")
|> assign(:page_title, t("Admin Dashboard"))
|> assign(:timezone, socket.assigns.current_scope.timezone)}
end

View file

@ -1,13 +1,13 @@
<Layouts.admin flash={@flash} timezone={@timezone}>
<div class="space-y-6">
<div>
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">Admin Dashboard</h1>
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">{t("Admin Dashboard")}</h1>
<p class="text-gray-600 dark:text-gray-400">System overview and recent activity</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<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">Users</h2>
<h2 class="text-lg font-semibold text-gray-900 dark:text-white mb-2">{t("Users")}</h2>
<p class="text-3xl font-bold text-gray-900 dark:text-white">{@user_count}</p>
<.link
navigate={~p"/admin/users"}
@ -18,7 +18,7 @@
</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">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"}
@ -33,7 +33,7 @@
<.icon name="hero-queue-list" class="w-5 h-5 inline mr-1" /> Oban Dashboard
</h2>
<p class="text-sm text-gray-600 dark:text-gray-400 mb-4">
Monitor background jobs and queues
{t("Monitor background jobs and queues")}
</p>
<.link
navigate={~p"/admin/oban"}
@ -48,7 +48,7 @@
<.icon name="hero-chart-bar" class="w-5 h-5 inline mr-1" /> LiveDashboard
</h2>
<p class="text-sm text-gray-600 dark:text-gray-400 mb-4">
System metrics and performance
{t("System metrics and performance")}
</p>
<.link
navigate={~p"/admin/dashboard"}
@ -63,7 +63,7 @@
<.icon name="hero-cpu-chip" class="w-5 h-5 inline mr-1" /> Job Monitoring
</h2>
<p class="text-sm text-gray-600 dark:text-gray-400 mb-4">
Real-time polling and discovery job monitoring
{t("Real-time polling and discovery job monitoring")}
</p>
<.link
navigate={~p"/admin/monitoring"}
@ -78,7 +78,7 @@
<.icon name="hero-server" class="w-5 h-5 inline mr-1" /> All Agents
</h2>
<p class="text-sm text-gray-600 dark:text-gray-400 mb-4">
Agents across all organizations and cloud pollers
{t("Agents across all organizations and cloud pollers")}
</p>
<.link
navigate={~p"/admin/agents"}
@ -91,7 +91,7 @@
<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">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"
@ -100,10 +100,10 @@
</.link>
</div>
<.table id="audit-logs" rows={@recent_logs}>
<:col :let={log} label="Action">{log.action}</:col>
<:col :let={log} label="User">{log.superuser && log.superuser.email}</:col>
<:col :let={log} label="Target">{log.target_user && log.target_user.email}</:col>
<:col :let={log} label="Time">
<:col :let={log} label={t("Action")}>{log.action}</:col>
<:col :let={log} label={t("User")}>{log.superuser && log.superuser.email}</:col>
<:col :let={log} label={t("Target")}>{log.target_user && log.target_user.email}</:col>
<:col :let={log} label={t("Time")}>
{ToweropsWeb.TimeHelpers.format_iso8601(log.inserted_at, @timezone)}
</:col>
</.table>

View file

@ -24,7 +24,7 @@ defmodule ToweropsWeb.Admin.MonitoringLive do
socket =
socket
|> assign(:page_title, "Job Monitoring")
|> assign(:page_title, t("Job Monitoring"))
|> assign(:active_jobs, [])
|> assign(:executing_jobs, [])
|> assign(:stuck_jobs, [])

View file

@ -2,10 +2,10 @@
<div class="space-y-6">
<div>
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">
Job Monitoring Dashboard
{t("Job Monitoring Dashboard")}
</h1>
<p class="text-gray-600 dark:text-gray-400">
Real-time monitoring of polling and discovery jobs
{t("Real-time monitoring of polling and discovery jobs")}
</p>
</div>
@ -28,19 +28,19 @@
</div>
<div class="bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10 p-6">
<h3 class="text-sm font-medium text-gray-600 dark:text-gray-400">Avg Duration</h3>
<h3 class="text-sm font-medium text-gray-600 dark:text-gray-400">{t("Avg Duration")}</h3>
<p class="text-3xl font-bold text-gray-900 dark:text-white mt-2">
<%= if Map.get(@health_metrics, :avg_execution_time_seconds) do %>
{Map.get(@health_metrics, :avg_execution_time_seconds) |> Float.round(1)}s
<% else %>
N/A
{t("N/A")}
<% end %>
</p>
<p class="text-sm text-gray-500 dark:text-gray-500 mt-1">last hour</p>
</div>
<div class="bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10 p-6">
<h3 class="text-sm font-medium text-gray-600 dark:text-gray-400">Active Jobs</h3>
<h3 class="text-sm font-medium text-gray-600 dark:text-gray-400">{t("Active Jobs")}</h3>
<p class="text-3xl font-bold text-gray-900 dark:text-white mt-2">
{length(@active_jobs)}
</p>
@ -162,13 +162,13 @@
<!-- Health Metrics -->
<div class="bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10 p-4">
<h2 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">
Health Metrics
{t("Health Metrics")}
</h2>
<!-- Current Activity -->
<div class="mb-6">
<h3 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">
Current Activity
{t("Current Activity")}
</h3>
<div class="grid grid-cols-2 gap-3">
<div class="bg-gray-50 dark:bg-gray-800 rounded p-3">
@ -201,7 +201,7 @@
<!-- Success Rates -->
<div class="mb-6">
<h3 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">
Success Rates
{t("Success Rates")}
</h3>
<div class="space-y-3">
<div>
@ -241,7 +241,7 @@
<!-- Performance -->
<div class="mb-6">
<h3 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">
Performance
{t("Performance")}
</h3>
<dl class="space-y-2 text-sm">
<div class="flex justify-between">
@ -250,7 +250,7 @@
<%= if @metrics.avg_execution_time_seconds do %>
{Float.round(@metrics.avg_execution_time_seconds, 1)}s
<% else %>
N/A
{t("N/A")}
<% end %>
</dd>
</div>
@ -266,7 +266,7 @@
<!-- Queue Depths -->
<div>
<h3 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">
Queue Depths
{t("Queue Depths")}
</h3>
<dl class="space-y-2 text-sm">
<div class="flex justify-between">
@ -286,7 +286,7 @@
<div class="mt-4 pt-4 border-t border-gray-200 dark:border-gray-700">
<p class="text-xs text-gray-500 dark:text-gray-500">
Updates every 10 seconds
{t("Updates every 10 seconds")}
</p>
</div>
</div>
@ -294,7 +294,7 @@
<!-- Recent Activity -->
<div class="bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10 p-4">
<h2 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">
Recent Activity
{t("Recent Activity")}
</h2>
<%= if length(@recent_events) == 0 do %>

View file

@ -12,7 +12,7 @@ defmodule ToweropsWeb.Admin.OrgLive.Index do
{:ok,
socket
|> assign(:page_title, "All Organizations")
|> assign(:page_title, t("All Organizations"))
|> assign(:timezone, socket.assigns.current_scope.timezone)
|> assign(:organizations, orgs)}
end

View file

@ -1,26 +1,26 @@
<Layouts.admin flash={@flash} timezone={@timezone}>
<div class="space-y-6">
<div>
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">All Organizations</h1>
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">{t("All Organizations")}</h1>
<p class="text-gray-600 dark:text-gray-400">Manage organizations</p>
</div>
<div class="bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10">
<.table id="organizations" rows={@organizations}>
<:col :let={org} label="Name">{org.name}</:col>
<:col :let={org} label="Slug">{org.slug}</:col>
<:col :let={org} label="Members">{length(org.memberships)}</:col>
<:col :let={org} label="Created">
<:col :let={org} label={t("Name")}>{org.name}</:col>
<:col :let={org} label={t("Slug")}>{org.slug}</:col>
<:col :let={org} label={t("Members")}>{length(org.memberships)}</:col>
<:col :let={org} label={t("Created")}>
{ToweropsWeb.TimeHelpers.format_date(org.inserted_at, @timezone)}
</:col>
<:col :let={org} label="">
<button
phx-click="delete_org"
phx-value-id={org.id}
data-confirm="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"
>
Delete
{t("Delete")}
</button>
</:col>
</.table>

View file

@ -19,7 +19,7 @@ defmodule ToweropsWeb.Admin.SecurityLive.Index do
{:ok,
socket
|> assign(:page_title, "Security - IP Access Control")
|> assign(:page_title, t("Security - IP Access Control"))
|> assign(:current_tab, "whitelist")}
end
@ -64,7 +64,7 @@ defmodule ToweropsWeb.Admin.SecurityLive.Index do
{:ok, _entry} ->
{:noreply,
socket
|> put_flash(:info, "IP/CIDR added to allowlist")
|> put_flash(:info, t("IP/CIDR added to allowlist"))
|> assign(:show_whitelist_form, false)
|> load_data()}
@ -72,7 +72,7 @@ defmodule ToweropsWeb.Admin.SecurityLive.Index do
{:noreply,
socket
|> assign(:whitelist_form, to_form(Map.put(changeset, :action, :validate)))
|> put_flash(:error, "Failed to add to allowlist")}
|> put_flash(:error, t("Failed to add to allowlist"))}
end
end
@ -81,11 +81,11 @@ defmodule ToweropsWeb.Admin.SecurityLive.Index do
:ok ->
{:noreply,
socket
|> put_flash(:info, "Removed from allowlist")
|> put_flash(:info, t("Removed from allowlist"))
|> load_data()}
{:error, _} ->
{:noreply, put_flash(socket, :error, "Failed to remove from allowlist")}
{:noreply, put_flash(socket, :error, t("Failed to remove from allowlist"))}
end
end
@ -94,11 +94,11 @@ defmodule ToweropsWeb.Admin.SecurityLive.Index do
{:ok, _} ->
{:noreply,
socket
|> put_flash(:info, "IP #{ip_address} unblocked")
|> put_flash(:info, t("IP %{ip_address} unblocked", ip_address: ip_address))
|> load_data()}
{:error, _} ->
{:noreply, put_flash(socket, :error, "Failed to unblock IP")}
{:noreply, put_flash(socket, :error, t("Failed to unblock IP"))}
end
end

View file

@ -1,9 +1,9 @@
<Layouts.admin flash={@flash} timezone={@timezone}>
<div class="space-y-6">
<div>
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">IP Access Control</h1>
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">{t("IP Access Control")}</h1>
<p class="text-gray-600 dark:text-gray-400">
Manage IP allowlist and view denied IPs
{t("Manage IP allowlist and view denied IPs")}
</p>
</div>
@ -22,7 +22,7 @@
)
]}
>
Allowlist
{t("Allowlist")}
</button>
<button
@ -37,7 +37,7 @@
)
]}
>
Denylist
{t("Denylist")}
</button>
</nav>
</div>
@ -47,7 +47,7 @@
<div class="space-y-4">
<div class="flex justify-between items-center">
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">
Allowed IPs and CIDR Ranges
{t("Allowed IPs and CIDR Ranges")}
</h2>
<.button phx-click="show_whitelist_form">
<.icon name="hero-plus" class="w-4 h-4 mr-1" /> Add to Allowlist
@ -65,7 +65,7 @@
<.input
field={@whitelist_form[:ip_or_cidr]}
type="text"
label="IP Address or CIDR"
label={t("IP Address or CIDR")}
placeholder="192.168.1.100 or 10.0.0.0/8"
required
/>
@ -73,15 +73,15 @@
<.input
field={@whitelist_form[:description]}
type="text"
label="Description"
placeholder="Office network"
label={t("Description")}
placeholder={t("Office network")}
/>
</div>
<div class="flex gap-2">
<.button type="submit">Add to Allowlist</.button>
<.button type="submit">{t("Add to Allowlist")}</.button>
<.button type="button" phx-click="hide_whitelist_form">
Cancel
{t("Cancel")}
</.button>
</div>
</.form>
@ -91,30 +91,30 @@
<div class="bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10">
<%= if Enum.empty?(@whitelist) do %>
<div class="p-8 text-center text-gray-500 dark:text-gray-400">
No allowed IPs or CIDR ranges
{t("No allowed IPs or CIDR ranges")}
</div>
<% else %>
<.table id="whitelist" rows={@whitelist}>
<:col :let={entry} label="IP/CIDR">{entry.ip_or_cidr}</:col>
<:col :let={entry} label="Description">{entry.description}</:col>
<:col :let={entry} label="Added By">
<:col :let={entry} label={t("IP/CIDR")}>{entry.ip_or_cidr}</:col>
<:col :let={entry} label={t("Description")}>{entry.description}</:col>
<:col :let={entry} label={t("Added By")}>
<%= if entry.added_by do %>
{entry.added_by.email}
<% else %>
<span class="text-gray-400">Unknown</span>
<% end %>
</:col>
<:col :let={entry} label="Added">
<:col :let={entry} label={t("Added")}>
{ToweropsWeb.TimeHelpers.format_date(entry.inserted_at, @timezone)}
</:col>
<:col :let={entry} label="">
<button
phx-click="remove_whitelist"
phx-value-id={entry.id}
data-confirm="Remove this IP/CIDR from the allowlist?"
data-confirm={t("Remove this IP/CIDR from the allowlist?")}
class="text-sm text-red-600 dark:text-red-400 hover:text-red-800 dark:hover:text-red-300"
>
Remove
{t("Remove")}
</button>
</:col>
</.table>
@ -127,7 +127,7 @@
<div class="space-y-4">
<div class="flex justify-between items-center">
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">
Denied IP Addresses
{t("Denied IP Addresses")}
</h2>
<div class="flex items-center gap-2">
@ -147,12 +147,12 @@
<div class="bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10">
<%= if Enum.empty?(@blocked_ips) do %>
<div class="p-8 text-center text-gray-500 dark:text-gray-400">
No denied IPs
{t("No denied IPs")}
</div>
<% else %>
<.table id="blocked_ips" rows={@blocked_ips}>
<:col :let={block} label="IP Address">{block.ip_address}</:col>
<:col :let={block} label="Offense Count">
<:col :let={block} label={t("IP Address")}>{block.ip_address}</:col>
<:col :let={block} label={t("Offense Count")}>
<span class={[
"inline-flex items-center px-2 py-1 rounded-full text-xs font-medium",
case block.offense_count do
@ -169,10 +169,10 @@
{block.offense_count}
</span>
</:col>
<:col :let={block} label="Status">
<:col :let={block} label={t("Status")}>
<%= if Towerops.Security.IpBlock.permanent?(block) do %>
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400">
Permanent
{t("Permanent")}
</span>
<% else %>
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-400">
@ -180,10 +180,10 @@
</span>
<% end %>
</:col>
<:col :let={block} label="Last Violation">
<:col :let={block} label={t("Last Violation")}>
{ToweropsWeb.TimeHelpers.format_time_ago(block.last_violation_at)}
</:col>
<:col :let={block} label="Reason">
<:col :let={block} label={t("Reason")}>
<div class="text-sm text-gray-600 dark:text-gray-400 max-w-xs truncate">
{block.reason}
</div>
@ -192,10 +192,10 @@
<button
phx-click="unblock_ip"
phx-value-ip={block.ip_address}
data-confirm="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"
>
Unblock
{t("Unblock")}
</button>
</:col>
</.table>

View file

@ -30,7 +30,7 @@ defmodule ToweropsWeb.Admin.UserLive.Index do
{:ok,
socket
|> assign(:page_title, "All Users")
|> assign(:page_title, t("All Users"))
|> assign(:timezone, socket.assigns.current_scope.timezone)
|> assign(:users, users)
|> assign(:client_ip, ip)}

View file

@ -1,17 +1,17 @@
<Layouts.admin flash={@flash} timezone={@timezone}>
<div class="space-y-6">
<div>
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">All Users</h1>
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">{t("All Users")}</h1>
<p class="text-gray-600 dark:text-gray-400">Manage system users</p>
</div>
<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="Email">{user.email}</:col>
<:col :let={user} label="Superuser">{if user.is_superuser, do: "Yes", else: "No"}</:col>
<:col :let={user} label="Organizations">{length(user.organizations)}</:col>
<:col :let={user} label="Devices">{user.device_count || 0}</:col>
<:col :let={user} label="Joined">
<: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("Organizations")}>{length(user.organizations)}</:col>
<:col :let={user} label={t("Devices")}>{user.device_count || 0}</:col>
<:col :let={user} label={t("Joined")}>
{ToweropsWeb.TimeHelpers.format_date(user.inserted_at, @timezone)}
</:col>
<:col :let={user} label="">
@ -23,17 +23,17 @@
disabled={user.id == @current_scope.user.id}
class="text-sm text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300 disabled:text-gray-400 disabled:cursor-not-allowed"
>
Impersonate
{t("Impersonate")}
</button>
</form>
<button
phx-click="delete_user"
phx-value-id={user.id}
data-confirm="Are you sure you want to delete this user?"
data-confirm={t("Are you sure you want to delete this user?")}
disabled={user.is_superuser}
class="text-sm text-red-600 dark:text-red-400 hover:text-red-800 dark:hover:text-red-300 disabled:text-gray-400 disabled:cursor-not-allowed"
>
Delete
{t("Delete")}
</button>
</div>
</:col>

View file

@ -12,10 +12,10 @@
<div class="bg-white dark:bg-gray-800/50 shadow rounded-lg">
<div class="px-6 py-5 border-b border-gray-200 dark:border-white/10">
<h3 class="text-lg font-medium text-gray-900 dark:text-white">
Edit Agent
{t("Edit Agent")}
</h3>
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400">
Update the agent name. The authentication token cannot be changed.
{t("Update the agent name. The authentication token cannot be changed.")}
</p>
</div>
@ -31,12 +31,12 @@
<.input
field={@form[:name]}
type="text"
label="Agent Name"
label={t("Agent Name")}
placeholder="e.g., Datacenter Agent"
required
/>
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">
A descriptive name to identify this agent in your organization.
{t("A descriptive name to identify this agent in your organization.")}
</p>
</div>
@ -44,7 +44,7 @@
<.input
field={@form[:allow_remote_debug]}
type="checkbox"
label="Allow Remote Debugging"
label={t("Allow Remote Debugging")}
/>
<div
@ -56,8 +56,8 @@
<div class="text-sm">
<p class="font-medium">Performance Impact</p>
<p class="mt-1">
When enabled, the server logs all SNMP data and messages for this agent.
This generates significant log volume and should only be enabled temporarily
{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.
</p>
</div>
@ -65,8 +65,8 @@
</div>
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">
Enable verbose logging for troubleshooting SNMP discovery and polling issues.
Only organization members can toggle this setting.
{t("Enable verbose logging for troubleshooting SNMP discovery and polling issues.")}
{t("Only organization members can toggle this setting.")}
</p>
</div>
</div>
@ -76,10 +76,10 @@
navigate={~p"/agents/#{@agent_token.id}"}
class="text-sm font-semibold text-gray-700 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white"
>
Cancel
{t("Cancel")}
</.link>
<.button type="submit" phx-disable-with="Saving...">
Save Changes
<.button type="submit" phx-disable-with={t("Saving...")}>
{t("Save Changes")}
</.button>
</div>
</.form>

View file

@ -53,7 +53,7 @@ defmodule ToweropsWeb.AgentLive.Index do
{:ok,
socket
|> assign(:page_title, "Remote Agents")
|> assign(:page_title, t("Remote Agents"))
|> assign(:timezone, socket.assigns.current_scope.timezone)
|> assign(:is_superuser, is_superuser)
|> stream(:agent_tokens, agent_tokens)

View file

@ -5,7 +5,7 @@
>
<.header>
{@page_title}
<:subtitle>Manage remote agents for local SNMP polling</:subtitle>
<:subtitle>{t("Manage remote agents for local SNMP polling")}</:subtitle>
<:actions>
<.button phx-click={JS.show(to: "#new-agent-form")} variant="primary">
<.icon name="hero-plus" class="h-5 w-5" /> New Agent
@ -17,14 +17,14 @@
<div id="new-agent-form" class="hidden mt-6">
<div class="bg-white dark:bg-gray-800/50 border border-gray-200 dark:border-white/10 rounded-lg p-6">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">
Create New Agent
{t("Create New Agent")}
</h3>
<.form for={@agent_form} phx-submit="create_agent">
<div class="space-y-4">
<.input
field={@agent_form[:name]}
type="text"
label="Agent Name"
label={t("Agent Name")}
placeholder="e.g., Datacenter A"
required
/>
@ -32,19 +32,19 @@
<.input
field={@agent_form[:is_cloud_poller]}
type="checkbox"
label="Cloud Poller (Application-wide agent, not tied to this organization)"
label={t("Cloud Poller (Application-wide agent, not tied to this organization)")}
/>
<% end %>
<div class="flex gap-3">
<.button type="submit" variant="primary">
Create Agent
{t("Create Agent")}
</.button>
<button
type="button"
phx-click={JS.hide(to: "#new-agent-form")}
class="px-3 py-2 text-sm font-semibold text-gray-700 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white"
>
Cancel
{t("Cancel")}
</button>
</div>
</div>
@ -55,9 +55,9 @@
<%= if !@has_agents 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">No agents</h3>
<h3 class="mt-4 text-lg font-semibold text-gray-900 dark:text-white">{t("No agents")}</h3>
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">
Get started by creating your first remote agent.
{t("Get started by creating your first remote agent.")}
</p>
<div class="mt-6">
<.button phx-click={JS.show(to: "#new-agent-form")} variant="primary">
@ -72,7 +72,7 @@
rows={@streams.agent_tokens}
row_link={fn {_id, agent} -> ~p"/agents/#{agent.id}" end}
>
<:col :let={{_id, agent}} label="Name">
<:col :let={{_id, agent}} label={t("Name")}>
<div class="flex items-center gap-2">
<span class="font-medium">{agent.name}</span>
<%= if agent.is_cloud_poller do %>
@ -88,7 +88,7 @@
</div>
</:col>
<:col :let={{_id, agent}} label="Status">
<:col :let={{_id, agent}} label={t("Status")}>
<% {status, label} = agent_status(agent) %>
<span class={"inline-flex items-center gap-1.5 rounded-full px-2.5 py-0.5 text-xs font-medium #{status_badge_class(status)}"}>
<span class={"h-1.5 w-1.5 rounded-full #{status_dot_class(status)}"} />
@ -96,7 +96,7 @@
</span>
</:col>
<:col :let={{_id, agent}} label="Device">
<:col :let={{_id, agent}} label={t("Device")}>
<% counts = Map.get(@device_counts, agent.id, %{direct: 0, total: 0}) %>
<div class="text-sm text-gray-900 dark:text-white">
{counts.total} total
@ -109,7 +109,7 @@
</div>
</:col>
<:col :let={{_id, agent}} label="Last Seen">
<:col :let={{_id, agent}} label={t("Last Seen")}>
<div class="text-sm text-gray-900 dark:text-white">
<.timestamp datetime={agent.last_seen_at} timezone={@timezone} now={@now} />
</div>
@ -125,7 +125,7 @@
<% end %>
</:col>
<:col :let={{_id, agent}} label="Version">
<:col :let={{_id, agent}} label={t("Version")}>
<%= if agent.metadata["version"] do %>
<div class="text-sm text-gray-600 dark:text-gray-400 font-mono">
{format_agent_version(agent.metadata["version"])}
@ -133,7 +133,7 @@
<% end %>
</:col>
<:col :let={{_id, agent}} label="Created">
<:col :let={{_id, agent}} label={t("Created")}>
<div class="text-sm text-gray-600 dark:text-gray-400">
{ToweropsWeb.TimeHelpers.format_date(agent.inserted_at, @timezone)}
</div>
@ -148,16 +148,16 @@
phx-value-id={agent.id}
class="text-sm font-medium text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"
>
View Setup
{t("View Setup")}
</button>
<button
type="button"
phx-click="delete_agent"
phx-value-id={agent.id}
data-confirm="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"
>
Delete
{t("Delete")}
</button>
</div>
<% else %>
@ -172,17 +172,17 @@
<%= if @is_superuser && @has_cloud_pollers do %>
<div class="mt-12">
<h2 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">
Cloud Pollers
{t("Cloud Pollers")}
</h2>
<p class="text-sm text-gray-600 dark:text-gray-400 mb-4">
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"
rows={@streams.cloud_pollers}
row_link={fn {_id, agent} -> ~p"/agents/#{agent.id}" end}
>
<:col :let={{_id, agent}} label="Name">
<:col :let={{_id, agent}} label={t("Name")}>
<div class="flex items-center gap-2">
<span class="font-medium">{agent.name}</span>
<%= if agent.id == @global_default_cloud_poller_id do %>
@ -198,7 +198,7 @@
</div>
</:col>
<:col :let={{_id, agent}} label="Status">
<:col :let={{_id, agent}} label={t("Status")}>
<% {status, label} = agent_status(agent) %>
<span class={"inline-flex items-center gap-1.5 rounded-full px-2.5 py-0.5 text-xs font-medium #{status_badge_class(status)}"}>
<span class={"h-1.5 w-1.5 rounded-full #{status_dot_class(status)}"} />
@ -206,7 +206,7 @@
</span>
</:col>
<:col :let={{_id, agent}} label="Device">
<:col :let={{_id, agent}} label={t("Device")}>
<% counts = Map.get(@cloud_poller_counts, agent.id, %{direct: 0, total: 0}) %>
<div class="text-sm text-gray-900 dark:text-white">
{counts.total} total
@ -219,7 +219,7 @@
</div>
</:col>
<:col :let={{_id, agent}} label="Last Seen">
<:col :let={{_id, agent}} label={t("Last Seen")}>
<div class="text-sm text-gray-900 dark:text-white">
<.timestamp datetime={agent.last_seen_at} timezone={@timezone} now={@now} />
</div>
@ -235,7 +235,7 @@
<% end %>
</:col>
<:col :let={{_id, agent}} label="Version">
<:col :let={{_id, agent}} label={t("Version")}>
<%= if agent.metadata["version"] do %>
<div class="text-sm text-gray-600 dark:text-gray-400 font-mono">
{format_agent_version(agent.metadata["version"])}
@ -243,7 +243,7 @@
<% end %>
</:col>
<:col :let={{_id, agent}} label="Created">
<:col :let={{_id, agent}} label={t("Created")}>
<div class="text-sm text-gray-600 dark:text-gray-400">
{ToweropsWeb.TimeHelpers.format_date(agent.inserted_at, @timezone)}
</div>
@ -258,16 +258,16 @@
phx-value-id={agent.id}
class="text-sm font-medium text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"
>
View Setup
{t("View Setup")}
</button>
<button
type="button"
phx-click="delete_agent"
phx-value-id={agent.id}
data-confirm="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"
>
Delete
{t("Delete")}
</button>
</div>
<% else %>
@ -283,17 +283,17 @@
<div class="mt-8">
<div class="bg-amber-50 dark:bg-amber-900/10 border border-amber-200 dark:border-amber-800/30 rounded-lg p-6">
<h3 class="text-lg font-semibold text-amber-900 dark:text-amber-200 mb-2">
Global Default Cloud Poller
{t("Global Default Cloud Poller")}
</h3>
<p class="text-sm text-amber-700 dark:text-amber-300 mb-4">
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">
<div class="space-y-4">
<div>
<label class="block text-sm font-medium text-amber-900 dark:text-amber-200 mb-2">
Select Global Default
{t("Select Global Default")}
</label>
<select
name="agent_token_id"
@ -323,13 +323,13 @@
<% else %>
<p class="text-xs text-red-600 dark:text-red-400">
<.icon name="hero-exclamation-triangle" class="h-4 w-4 inline" />
Warning: Selected agent no longer exists. Please choose a new agent.
{t("Warning: Selected agent no longer exists. Please choose a new agent.")}
</p>
<% end %>
<% else %>
<p class="text-xs text-amber-600 dark:text-amber-400">
<.icon name="hero-information-circle" class="h-4 w-4 inline" />
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 %>
@ -352,7 +352,7 @@
}
class="px-3 py-2 text-sm font-semibold text-gray-700 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white"
>
Cancel
{t("Cancel")}
</button>
<% end %>
</div>
@ -381,10 +381,10 @@
<div class="flex items-start justify-between mb-4">
<div>
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">
Agent Setup Instructions
{t("Agent Setup Instructions")}
</h3>
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400">
Use the token and Docker Compose configuration below to deploy this agent.
{t("Use the token and Docker Compose configuration below to deploy this agent.")}
</p>
</div>
<button
@ -399,7 +399,7 @@
<div class="space-y-4">
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Agent Name
{t("Agent Name")}
</label>
<div class="text-base font-mono text-gray-900 dark:text-white">
{@new_token.agent_token.name}
@ -408,7 +408,7 @@
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Authentication Token
{t("Authentication Token")}
</label>
<div class="relative">
<input
@ -427,20 +427,20 @@
class="absolute right-2 top-1/2 -translate-y-1/2 px-3 py-1.5 text-xs font-medium text-white bg-blue-600 hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600 rounded shadow-sm transition-colors flex items-center gap-1.5"
>
<.icon name="hero-clipboard" class="h-3.5 w-3.5" />
<span>Copy</span>
<span>{t("Copy")}</span>
</button>
</div>
<p class="mt-1.5 text-xs text-gray-500 dark:text-gray-400">
Click the token to select all, or use the Copy button
{t("Click the token to select all, or use the Copy button")}
</p>
</div>
<div class="bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg p-4">
<h4 class="text-sm font-medium text-gray-900 dark:text-white mb-2">
Docker Compose Setup with Auto-Updates
{t("Docker Compose Setup with Auto-Updates")}
</h4>
<p class="text-xs text-gray-600 dark:text-gray-400 mb-3">
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:
@ -509,7 +509,7 @@
<div class="mt-6 flex justify-end">
<.button phx-click="close_token_modal" variant="primary">
Close
{t("Close")}
</.button>
</div>
</div>

View file

@ -52,7 +52,7 @@ defmodule ToweropsWeb.AgentLive.Show do
if socket.assigns.current_scope.user.is_superuser do
Agents.restart_agent(socket.assigns.agent_token.id)
{:noreply, put_flash(socket, :info, "Restart command sent to agent")}
{:noreply, put_flash(socket, :info, t("Restart command sent to agent"))}
else
{:noreply, socket}
end
@ -73,13 +73,13 @@ defmodule ToweropsWeb.AgentLive.Show do
with {:ok, release} <- ReleaseChecker.get_latest_release(),
{:ok, asset} <- ReleaseChecker.asset_for_arch(release.assets, arch) do
Agents.update_agent(agent_token.id, asset.url, asset.checksum)
put_flash(socket, :info, "Update command sent to agent (v#{release.version})")
put_flash(socket, :info, t("Update command sent to agent (v%{version})", version: release.version))
else
{:error, :no_matching_asset} ->
put_flash(socket, :error, "No binary available for architecture: #{arch}")
put_flash(socket, :error, t("No binary available for architecture: %{arch}", arch: arch))
{:error, _reason} ->
put_flash(socket, :error, "Failed to fetch latest release from GitHub")
put_flash(socket, :error, t("Failed to fetch latest release from GitHub"))
end
end

View file

@ -5,12 +5,12 @@
>
<.header>
{@page_title}
<:subtitle>Agent Details and Health</:subtitle>
<:subtitle>{t("Agent Details and Health")}</:subtitle>
<:actions>
<button
:if={@current_scope.user.is_superuser}
phx-click="update_agent"
data-confirm="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 +18,7 @@
<button
:if={@current_scope.user.is_superuser}
phx-click="restart_agent"
data-confirm="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
@ -57,7 +57,7 @@
)
]}
>
Overview
{t("Overview")}
</button>
<button
type="button"
@ -72,7 +72,7 @@
)
]}
>
Debug Logs
{t("Debug Logs")}
</button>
</nav>
</div>
@ -93,21 +93,21 @@
</p>
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400">
<%= if @agent_token.allow_remote_debug do %>
Verbose logging is enabled. All SNMP data and agent messages are being logged.
{t("Verbose logging is enabled. All SNMP data and agent messages are being logged.")}
<% else %>
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>
<div class="space-y-4">
<h3 class="text-sm font-medium text-gray-900 dark:text-white">
View Debug Logs
{t("View Debug Logs")}
</h3>
<div class="rounded-md bg-gray-50 dark:bg-gray-900 p-4">
<p class="text-sm mb-2 font-medium text-gray-900 dark:text-white">
Production (kubectl):
{t("Production (kubectl):")}
</p>
<code class="text-xs text-gray-800 dark:text-gray-200 block">
kubectl logs -n towerops deployment/towerops --tail=100 | grep "agent_token_id: {@agent_token.id}"
@ -116,7 +116,7 @@
<div class="rounded-md bg-gray-50 dark:bg-gray-900 p-4">
<p class="text-sm mb-2 font-medium text-gray-900 dark:text-white">
Development (tail):
{t("Development (tail):")}
</p>
<code class="text-xs text-gray-800 dark:text-gray-200 block">
tail -f _build/dev/lib/towerops/priv/log/dev.log | grep "{@agent_token.id}"
@ -125,7 +125,7 @@
<div class="mt-4">
<p class="text-sm font-medium mb-2 text-gray-900 dark:text-white">
What Gets Logged:
{t("What Gets Logged:")}
</p>
<ul class="text-sm text-gray-600 dark:text-gray-400 list-disc list-inside space-y-1">
<li>Agent connection/disconnection events</li>
@ -215,7 +215,7 @@
</p>
<% else %>
<p class="mt-2 text-lg font-semibold text-gray-500 dark:text-gray-400">
Unknown
{t("Unknown")}
</p>
<% end %>
</div>
@ -228,7 +228,7 @@
<%= if @agent_token.metadata && map_size(@agent_token.metadata) > 0 do %>
<div class="mt-6 bg-white dark:bg-gray-800/50 border border-gray-200 dark:border-white/10 rounded-lg p-6">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">
Agent Details
{t("Agent Details")}
</h3>
<dl class="grid grid-cols-1 gap-4 sm:grid-cols-2">
<%= if @agent_token.metadata["version"] do %>
@ -254,7 +254,7 @@
<!-- Agent Timestamps -->
<div class="mt-6 bg-white dark:bg-gray-800/50 border border-gray-200 dark:border-white/10 rounded-lg p-6">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">
Timestamps
{t("Timestamps")}
</h3>
<dl class="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div>
@ -273,7 +273,7 @@
<div>
<dt class="text-sm font-medium text-gray-600 dark:text-gray-400">
Last Seen (Heartbeat)
{t("Last Seen (Heartbeat)")}
</dt>
<dd class="mt-1 text-sm text-gray-900 dark:text-white">
<.timestamp datetime={@agent_token.last_seen_at} timezone={@timezone} />
@ -295,10 +295,10 @@
<div class="mt-6 bg-white dark:bg-gray-800/50 border border-gray-200 dark:border-white/10 rounded-lg">
<div class="p-6 border-b border-gray-200 dark:border-white/10">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">
Polling Targets
{t("Polling Targets")}
</h3>
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400">
Device that this agent is responsible for polling
{t("Device that this agent is responsible for polling")}
</p>
</div>
@ -306,10 +306,10 @@
<div class="p-12 text-center">
<.icon name="hero-server" class="mx-auto h-12 w-12 text-gray-400 dark:text-gray-500" />
<h3 class="mt-4 text-sm font-semibold text-gray-900 dark:text-white">
No devices assigned
{t("No devices assigned")}
</h3>
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">
This agent is not currently polling any device.
{t("This agent is not currently polling any device.")}
</p>
</div>
<% else %>

View file

@ -23,7 +23,7 @@ defmodule ToweropsWeb.AlertLive.Index do
{:ok,
socket
|> assign(:page_title, "Alerts")
|> assign(:page_title, t("Alerts"))
|> assign(:filter, "unresolved")
|> assign(:sort_by, "severity")
|> assign(:expanded_sites, MapSet.new())

View file

@ -7,7 +7,7 @@
<span class="flex items-center gap-2">
Alerts <span class="badge badge-info badge-sm">Experimental</span>
</span>
<:subtitle>Triage alerts by site impact — fix the biggest problems first</:subtitle>
<:subtitle>{t("Triage alerts by site impact — fix the biggest problems first")}</:subtitle>
</.header>
<%!-- Filter Tabs --%>
@ -45,19 +45,19 @@
patch={~p"/alerts?filter=#{@filter}&sort=severity"}
class={["btn btn-xs", (@sort_by == "severity" && "btn-primary") || "btn-ghost"]}
>
Severity
{t("Severity")}
</.link>
<.link
patch={~p"/alerts?filter=#{@filter}&sort=age"}
class={["btn btn-xs", (@sort_by == "age" && "btn-primary") || "btn-ghost"]}
>
Oldest First
{t("Oldest First")}
</.link>
<.link
patch={~p"/alerts?filter=#{@filter}&sort=impact"}
class={["btn btn-xs", (@sort_by == "impact" && "btn-primary") || "btn-ghost"]}
>
Subscriber Impact
{t("Subscriber Impact")}
</.link>
</div>
@ -71,17 +71,17 @@
class="h-12 w-12 text-green-500 dark:text-green-400"
/>
</div>
<h3 class="card-title text-green-700 dark:text-green-400">All Clear!</h3>
<h3 class="card-title text-green-700 dark:text-green-400">{t("All Clear!")}</h3>
<p class="text-sm text-gray-600 dark:text-gray-400">
<%= case @filter do %>
<% "unresolved" -> %>
No active alerts. Your network is looking good!
{t("No active alerts. Your network is looking good!")}
<% "critical" -> %>
No critical alerts. Nice work keeping things healthy!
{t("No critical alerts. Nice work keeping things healthy!")}
<% "resolved" -> %>
No resolved alerts yet.
{t("No resolved alerts yet.")}
<% _ -> %>
No active alerts. Your network is looking good!
{t("No active alerts. Your network is looking good!")}
<% end %>
</p>
</div>
@ -294,7 +294,7 @@
<.button
phx-click="resolve"
phx-value-id={alert.id}
data-confirm="Resolve this alert?"
data-confirm={t("Resolve this alert?")}
>
<.icon name="hero-check-circle" class="h-4 w-4" /> Resolve
</.button>

View file

@ -32,7 +32,7 @@ defmodule ToweropsWeb.ConfigTimelineLive do
{:error, _} ->
{:ok,
socket
|> put_flash(:error, "Device not found")
|> put_flash(:error, t("Device not found"))
|> redirect(to: ~p"/devices")}
end
end

View file

@ -56,10 +56,10 @@ defmodule ToweropsWeb.DashboardLive do
{:noreply,
socket
|> load_insights(socket.assigns.current_scope.organization.id)
|> put_flash(:info, "Insight dismissed")}
|> put_flash(:info, t("Insight dismissed"))}
{:error, _} ->
{:noreply, put_flash(socket, :error, "Failed to dismiss insight")}
{:noreply, put_flash(socket, :error, t("Failed to dismiss insight"))}
end
end
@ -75,14 +75,14 @@ defmodule ToweropsWeb.DashboardLive do
{:noreply,
socket
|> load_dashboard_data(organization.id)
|> put_flash(:info, "Alert acknowledged")}
|> put_flash(:info, t("Alert acknowledged"))}
{:error, _} ->
{:noreply, put_flash(socket, :error, "Failed to acknowledge alert")}
{:noreply, put_flash(socket, :error, t("Failed to acknowledge alert"))}
end
{:error, _} ->
{:noreply, put_flash(socket, :error, "Alert not found")}
{:noreply, put_flash(socket, :error, t("Alert not found"))}
end
end

View file

@ -39,10 +39,10 @@
class="mx-auto h-14 w-14 text-blue-500 dark:text-blue-400"
/>
<h3 class="mt-6 text-xl font-semibold text-gray-900 dark:text-white">
Let's get started!
{t("Let's get started!")}
</h3>
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400 max-w-md mx-auto">
Set up your network monitoring in a few steps.
{t("Set up your network monitoring in a few steps.")}
</p>
<div class="mt-10 max-w-lg mx-auto">
<div class="space-y-5 text-left">
@ -52,9 +52,9 @@
1
</div>
<div>
<h4 class="font-medium text-gray-900 dark:text-white">Create a Site</h4>
<h4 class="font-medium text-gray-900 dark:text-white">{t("Create a Site")}</h4>
<p class="mt-0.5 text-sm text-gray-500 dark:text-gray-400">
Sites represent your physical locations
{t("Sites represent your physical locations")}
</p>
</div>
</div>
@ -63,9 +63,9 @@
2
</div>
<div>
<h4 class="font-medium text-gray-900 dark:text-white">Add Devices</h4>
<h4 class="font-medium text-gray-900 dark:text-white">{t("Add Devices")}</h4>
<p class="mt-0.5 text-sm text-gray-500 dark:text-gray-400">
Add routers, switches, and servers to your sites
{t("Add routers, switches, and servers to your sites")}
</p>
</div>
</div>
@ -75,9 +75,9 @@
1
</div>
<div>
<h4 class="font-medium text-gray-900 dark:text-white">Add Devices</h4>
<h4 class="font-medium text-gray-900 dark:text-white">{t("Add Devices")}</h4>
<p class="mt-0.5 text-sm text-gray-500 dark:text-gray-400">
Add network devices to monitor
{t("Add network devices to monitor")}
</p>
</div>
</div>
@ -88,10 +88,10 @@
</div>
<div>
<h4 class="font-medium text-gray-900 dark:text-white">
Monitor & Receive Alerts
{t("Monitor & Receive Alerts")}
</h4>
<p class="mt-0.5 text-sm text-gray-500 dark:text-gray-400">
TowerOps automatically monitors your devices and alerts you of issues
{t("TowerOps automatically monitors your devices and alerts you of issues")}
</p>
</div>
</div>
@ -123,7 +123,7 @@
]}>
<div class="flex items-center justify-between">
<span class="text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400">
Health
{t("Health")}
</span>
<span class={[
"inline-flex items-center rounded-full px-1.5 py-0.5 text-[10px] font-semibold",
@ -156,7 +156,7 @@
<%!-- Uptime --%>
<div class={["rounded-lg border px-4 py-3", uptime_bg(@uptime_percentage)]}>
<span class="text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400">
Uptime
{t("Uptime")}
</span>
<p class={["mt-1 text-3xl font-bold tracking-tight", uptime_color(@uptime_percentage)]}>
{if @uptime_percentage == 100.0,
@ -174,7 +174,7 @@
class="rounded-lg border border-gray-200 bg-white px-4 py-3 transition hover:shadow-sm dark:border-white/10 dark:bg-gray-800/50"
>
<span class="text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400">
Devices
{t("Devices")}
</span>
<p class="mt-1 text-3xl font-bold tracking-tight text-gray-900 dark:text-white">
{@device_count}
@ -197,7 +197,7 @@
class="rounded-lg border border-gray-200 bg-white px-4 py-3 transition hover:shadow-sm dark:border-white/10 dark:bg-gray-800/50"
>
<span class="text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400">
Alerts
{t("Alerts")}
</span>
<p class={[
"mt-1 text-3xl font-bold tracking-tight",
@ -220,7 +220,7 @@
<%= if @has_subscribers do %>
<div class="rounded-lg border border-gray-200 bg-white px-4 py-3 dark:border-white/10 dark:bg-gray-800/50">
<span class="text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400">
Subscribers
{t("Subscribers")}
</span>
<p class="mt-1 text-2xl font-bold tracking-tight text-gray-900 dark:text-white">
{format_number(@summary.subscribers.total)}
@ -238,7 +238,7 @@
)
]}>
<span class="text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400">
MRR at Risk
{t("MRR at Risk")}
</span>
<p class={[
"mt-1 text-2xl font-bold tracking-tight",
@ -264,7 +264,7 @@
class="rounded-lg border border-gray-200 bg-white px-4 py-3 transition hover:shadow-sm dark:border-white/10 dark:bg-gray-800/50"
>
<span class="text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400">
Sites
{t("Sites")}
</span>
<p class="mt-1 text-2xl font-bold tracking-tight text-gray-900 dark:text-white">
{@sites_count}
@ -277,7 +277,7 @@
class="rounded-lg border border-gray-200 bg-white px-4 py-3 transition hover:shadow-sm dark:border-white/10 dark:bg-gray-800/50"
>
<span class="text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400">
Insights
{t("Insights")}
</span>
<p class="mt-1 text-2xl font-bold tracking-tight text-gray-900 dark:text-white">
{@summary.insights.critical + @summary.insights.warning + @summary.insights.info}
@ -301,13 +301,13 @@
<% end %>
<%!-- ═══════════════════════════════════════════════
ACTIVE INCIDENTS (only when there ARE incidents)
{t("ACTIVE INCIDENTS (only when there ARE incidents)")}
═══════════════════════════════════════════════ --%>
<%= if @active_incidents != [] do %>
<div class="mt-8">
<div class="flex items-center gap-2 mb-3">
<h2 class="text-sm font-semibold uppercase tracking-wide text-red-600 dark:text-red-400">
Active Incidents
{t("Active Incidents")}
</h2>
<span class="inline-flex items-center rounded-full bg-red-100 px-2 py-0.5 text-xs font-semibold text-red-700 dark:bg-red-900/30 dark:text-red-400">
{length(@active_incidents)}
@ -355,7 +355,7 @@
<% end %>
<%!-- ═══════════════════════════════════════════════
MAIN CONTENT — full width stacked layout
{t("MAIN CONTENT — full width stacked layout")}
═══════════════════════════════════════════════ --%>
<%!-- Active Alerts (only show section when there ARE alerts) --%>
@ -363,7 +363,7 @@
<div class="mt-8">
<div class="flex items-center justify-between mb-3">
<h2 class="text-sm font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">
Active Alerts
{t("Active Alerts")}
</h2>
<.link
navigate={~p"/alerts"}
@ -377,16 +377,16 @@
<thead class="bg-gray-50 dark:bg-gray-800/80">
<tr>
<th class="px-4 py-2 text-left text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400">
Device
{t("Device")}
</th>
<th class="px-4 py-2 text-left text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400">
Message
{t("Message")}
</th>
<th class="px-4 py-2 text-left text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400">
Impact
{t("Impact")}
</th>
<th class="px-4 py-2 text-left text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400">
When
{t("When")}
</th>
<th class="px-4 py-2"></th>
</tr>
@ -429,7 +429,7 @@
phx-value-id={alert.id}
class="rounded px-2 py-1 text-xs font-medium text-gray-600 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 dark:text-gray-300 dark:ring-gray-600 dark:hover:bg-gray-700"
>
Ack
{t("Ack")}
</button>
<% else %>
<span class="text-xs text-blue-600 dark:text-blue-400">Ack'd</span>
@ -448,7 +448,7 @@
<div class="flex items-center justify-between mb-3">
<div class="flex items-center gap-3">
<h2 class="text-sm font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">
Insights
{t("Insights")}
</h2>
<div class="flex items-center gap-1">
<.link
@ -462,7 +462,7 @@
)
]}
>
All
{t("All")}
</.link>
<.link
:for={source <- ~w(preseem snmp gaiia system)}
@ -583,7 +583,7 @@
<div class="mt-8">
<div class="flex items-center justify-between mb-3">
<h2 class="text-sm font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">
Site Health
{t("Site Health")}
</h2>
<.link
navigate={~p"/sites"}
@ -597,19 +597,19 @@
<thead class="bg-gray-50 dark:bg-gray-800/80">
<tr>
<th class="px-4 py-2 text-left text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400">
Site
{t("Site")}
</th>
<th class="px-4 py-2 text-right text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400">
Devices
{t("Devices")}
</th>
<th class="px-4 py-2 text-right text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400">
Down
{t("Down")}
</th>
<th class="px-4 py-2 text-right text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400">
QoE
{t("QoE")}
</th>
<th class="px-4 py-2 text-right text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400">
Subscribers
{t("Subscribers")}
</th>
</tr>
</thead>
@ -680,7 +680,7 @@
<div class="mt-8">
<div class="flex items-center justify-between mb-3">
<h2 class="text-sm font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">
Recent Activity
{t("Recent Activity")}
</h2>
<.link
navigate={~p"/activity"}

View file

@ -89,7 +89,7 @@ defmodule ToweropsWeb.DeviceLive.Form do
changeset = Devices.change_device(%DeviceSchema{}, equipment_attrs)
socket
|> assign(:page_title, "New Device")
|> assign(:page_title, t("New Device"))
|> assign(:device, %DeviceSchema{})
|> assign(:form, to_form(changeset))
|> assign(:monitoring_mode, "snmp_and_icmp")
@ -159,7 +159,7 @@ defmodule ToweropsWeb.DeviceLive.Form do
monitoring_mode = if device.snmp_enabled, do: "snmp_and_icmp", else: "icmp_only"
socket
|> assign(:page_title, "Edit Device")
|> assign(:page_title, t("Edit Device"))
|> assign(:device, device_with_agent)
|> assign(:form, to_form(changeset))
|> assign(:agent_source, agent_source)

View file

@ -82,7 +82,7 @@
<.icon name="hero-exclamation-triangle" class="h-5 w-5 text-orange-400" />
<div class="ml-3">
<h3 class="text-sm font-medium text-orange-800 dark:text-orange-400">
Approaching device limit
{t("Approaching device limit")}
</h3>
<p class="mt-1 text-sm text-orange-700 dark:text-orange-300">
You have {remaining} {if remaining == 1, do: "slot", else: "slots"} remaining.
@ -100,10 +100,10 @@
<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">
<div>
<h2 class="text-base/7 font-semibold text-gray-900 dark:text-white">
Basic Information
{t("Basic Information")}
</h2>
<p class="mt-1 text-sm/6 text-gray-500 dark:text-gray-400">
Core device identification and location details.
{t("Core device identification and location details.")}
</p>
</div>
@ -113,7 +113,7 @@
<.input
field={@form[:name]}
type="text"
label="Device Name"
label={t("Device Name")}
placeholder={
if @monitoring_mode == "snmp_and_icmp",
do: "Leave blank to automatically populate from SNMP device name (sysName)",
@ -124,7 +124,7 @@
</div>
<div class="col-span-full">
<.input field={@form[:ip_address]} type="text" label="IP Address" required />
<.input field={@form[:ip_address]} type="text" label={t("IP Address")} required />
<div
:if={@duplicate_device}
@ -133,7 +133,7 @@
<div class="flex items-center gap-2 text-amber-800 dark:text-amber-200">
<.icon name="hero-exclamation-triangle" class="h-5 w-5 flex-shrink-0" />
<p class="text-sm">
A device with this IP address already exists:
{t("A device with this IP address already exists:")}
<.link
navigate={~p"/devices/#{@duplicate_device.id}"}
class="font-medium underline hover:text-amber-900 dark:hover:text-amber-100"
@ -153,8 +153,8 @@
<div class="text-sm">
<p class="font-medium">Non-routable IP address detected</p>
<p class="mt-1">
This IP address (private network or CGNAT range) cannot be reached from the cloud.
To monitor this device, assign it to a
{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
</.link>
@ -169,24 +169,24 @@
<.input
field={@form[:site_id]}
type="select"
label="Site (Optional)"
label={t("Site (Optional)")}
prompt="No site (ungrouped)"
options={Enum.map(@available_sites, &{&1.name, &1.id})}
/>
<p class="mt-1 text-sm text-gray-500">
Optionally group this device into a site (office, datacenter, etc.)
{t("Optionally group this device into a site (office, datacenter, etc.)")}
</p>
</div>
<div class="col-span-full">
<.input field={@form[:description]} type="textarea" label="Description" />
<.input field={@form[:description]} type="textarea" label={t("Description")} />
</div>
<div class="col-span-full">
<.input
field={@form[:device_role]}
type="select"
label="Device Role"
label={t("Device Role")}
prompt="Auto-detect"
options={[
{"Router", "router"},
@ -202,7 +202,7 @@
]}
/>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
Select a role manually or leave as auto-detect to infer from SNMP data.
{t("Select a role manually or leave as auto-detect to infer from SNMP data.")}
</p>
</div>
</div>
@ -213,10 +213,10 @@
<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">
<div>
<h2 class="text-base/7 font-semibold text-gray-900 dark:text-white">
Monitoring Configuration
{t("Monitoring Configuration")}
</h2>
<p class="mt-1 text-sm/6 text-gray-500 dark:text-gray-400">
Configure how and how often this device should be monitored.
{t("Configure how and how often this device should be monitored.")}
</p>
</div>
@ -226,7 +226,7 @@
<.input
field={@form[:check_interval_seconds]}
type="number"
label="Check Interval (seconds)"
label={t("Check Interval (seconds)")}
min="30"
max="3600"
/>
@ -236,7 +236,7 @@
<.input
field={@form[:monitoring_enabled]}
type="checkbox"
label="Enable Monitoring"
label={t("Enable Monitoring")}
/>
</div>
@ -245,7 +245,7 @@
<.input
field={@form[:agent_token_id]}
type="select"
label="Remote Agent"
label={t("Remote Agent")}
prompt="Inherit from site/organization"
options={
Enum.map(@available_agents, fn agent ->
@ -263,7 +263,7 @@
<% :device -> %>
<p class="mt-2 text-sm text-amber-600 dark:text-amber-400 flex items-center gap-1">
<.icon name="hero-exclamation-triangle" class="h-4 w-4" />
<strong>Overriding</strong>
<strong>{t("Overriding")}</strong>
site/organization defaults - this device is explicitly assigned
</p>
<% :site -> %>
@ -279,18 +279,18 @@
<% :global -> %>
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400 flex items-center gap-1">
<.icon name="hero-globe-alt" class="h-4 w-4" />
Using global default cloud poller:
{t("Using global default cloud poller:")}
<strong>{@effective_agent_name}</strong>
</p>
<% :none -> %>
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400 flex items-center gap-1">
<.icon name="hero-cloud" class="h-4 w-4" />
No agent assigned - cloud polling
{t("No agent assigned - cloud polling")}
</p>
<% end %>
<% else %>
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">
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>
@ -303,10 +303,10 @@
<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">
<div>
<h2 class="text-base/7 font-semibold text-gray-900 dark:text-white">
SNMP Configuration
{t("SNMP Configuration")}
</h2>
<p class="mt-1 text-sm/6 text-gray-500 dark:text-gray-400">
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>
@ -315,7 +315,7 @@
<!-- Monitoring Mode Tabs -->
<div>
<div class="border-b border-gray-200 dark:border-white/10">
<nav class="-mb-px flex space-x-8" aria-label="Monitoring mode">
<nav class="-mb-px flex space-x-8" aria-label={t("Monitoring mode")}>
<button
type="button"
phx-click="switch_monitoring_mode"
@ -328,7 +328,7 @@
"border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 dark:text-gray-400 dark:hover:border-gray-600 dark:hover:text-gray-300"
]}
>
SNMP & ICMP
{t("SNMP & ICMP")}
</button>
<button
type="button"
@ -342,14 +342,14 @@
"border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 dark:text-gray-400 dark:hover:border-gray-600 dark:hover:text-gray-300"
]}
>
ICMP Only
{t("ICMP Only")}
</button>
</nav>
</div>
<div class="mt-2 text-sm text-gray-600 dark:text-gray-400">
<%= if @monitoring_mode == "snmp_and_icmp" do %>
<p>
Monitor device availability with ICMP pings and collect detailed information via SNMP
{t("Monitor device availability with ICMP pings and collect detailed information via SNMP")}
(interfaces, sensors, system info).
</p>
<% else %>
@ -367,7 +367,7 @@
<.input
field={@form[:snmp_version]}
type="select"
label="SNMP Version"
label={t("SNMP Version")}
prompt="Inherit from site/organization"
options={[{"v1", "1"}, {"v2c", "2c"}, {"v3", "3"}]}
/>
@ -381,8 +381,8 @@
<.input
field={@form[:snmp_community]}
type="text"
label="Community String"
placeholder="Leave blank to inherit from site/organization"
label={t("Community String")}
placeholder={t("Leave blank to inherit from site/organization")}
autocomplete="off"
/>
@ -391,7 +391,7 @@
<% :device -> %>
<p class="mt-2 text-sm text-amber-600 dark:text-amber-400 flex items-center gap-1">
<.icon name="hero-exclamation-triangle" class="h-4 w-4" />
<strong>Overriding</strong> site/organization SNMP defaults
<strong>{t("Overriding")}</strong> site/organization SNMP defaults
</p>
<% :site -> %>
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400 flex items-center gap-1">
@ -418,7 +418,7 @@
<% :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" />
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 %>
@ -430,8 +430,8 @@
<.input
field={@form[:snmpv3_username]}
type="text"
label="Username"
placeholder="Leave blank to inherit from site/organization"
label={t("Username")}
placeholder={t("Leave blank to inherit from site/organization")}
/>
</div>
@ -458,7 +458,7 @@
<.input
field={@form[:snmpv3_security_level]}
type="select"
label="Auth Level"
label={t("Auth Level")}
options={[
{"No Authentication, No Privacy", "noAuthNoPriv"},
{"Authentication, No Privacy", "authNoPriv"},
@ -473,7 +473,7 @@
<.input
field={@form[:snmpv3_auth_protocol]}
type="select"
label="Auth Protocol"
label={t("Auth Protocol")}
options={[
{"SHA-256 (recommended)", "SHA-256"},
{"SHA-512", "SHA-512"},
@ -489,8 +489,8 @@
<.input
field={@form[:snmpv3_auth_password]}
type="password"
label="Auth Password"
placeholder="Min 8 characters"
label={t("Auth Password")}
placeholder={t("Min 8 characters")}
autocomplete="off"
/>
</div>
@ -502,7 +502,7 @@
<.input
field={@form[:snmpv3_priv_protocol]}
type="select"
label="Privacy Protocol"
label={t("Privacy Protocol")}
options={[
{"AES-128 (recommended)", "AES"},
{"AES-256", "AES-256"},
@ -517,8 +517,8 @@
<.input
field={@form[:snmpv3_priv_password]}
type="password"
label="Privacy Password"
placeholder="Min 8 characters"
label={t("Privacy Password")}
placeholder={t("Min 8 characters")}
autocomplete="off"
/>
</div>
@ -529,7 +529,7 @@
<.input
field={@form[:snmp_port]}
type="number"
label="SNMP Port"
label={t("SNMP Port")}
min="1"
max="65535"
value={@form[:snmp_port].value || 161}
@ -538,15 +538,15 @@
<div class="col-span-full">
<div class="flex gap-3">
<.button type="button" phx-click="test_snmp" phx-disable-with="Testing...">
Test SNMP Connection
<.button type="button" phx-click="test_snmp" phx-disable-with={t("Testing...")}>
{t("Test SNMP Connection")}
</.button>
<%= if @live_action == :edit do %>
<.button
type="button"
phx-click="trigger_discovery"
phx-disable-with="Discovering..."
phx-disable-with={t("Discovering...")}
>
<.icon name="hero-magnifying-glass" class="h-4 w-4" /> Rediscover Device
</.button>
@ -566,7 +566,7 @@
</div>
<div class="ml-3">
<p class="text-sm text-blue-700 dark:text-blue-400">
Testing SNMP credentials...
{t("Testing SNMP credentials...")}
</p>
</div>
</div>
@ -579,7 +579,7 @@
</div>
<div class="ml-3">
<p class="text-sm font-medium text-green-800 dark:text-green-400">
Connection successful!
{t("Connection successful!")}
</p>
<p class="mt-1 text-sm text-green-700 dark:text-green-300">
{Map.get(@snmp_test_result, :message)}
@ -595,7 +595,7 @@
</div>
<div class="ml-3">
<p class="text-sm font-medium text-red-800 dark:text-red-400">
Connection failed
{t("Connection failed")}
</p>
<p class="mt-1 text-sm text-red-700 dark:text-red-300">
{Map.get(@snmp_test_result, :message)}
@ -620,16 +620,16 @@
>
<div>
<h2 class="text-base/7 font-semibold text-gray-900 dark:text-white">
MikroTik API Configuration
{t("MikroTik API Configuration")}
</h2>
<p class="mt-1 text-sm/6 text-gray-500 dark:text-gray-400">
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">
<.icon name="hero-beaker" class="h-4 w-4 inline" />
<strong>Experimental Feature:</strong>
MikroTik API integration is under active development.
<strong>{t("Experimental Feature:")}</strong>
{t("MikroTik API integration is under active development.")}
</p>
</div>
</div>
@ -650,7 +650,7 @@
<.input
field={@form[:mikrotik_enabled]}
type="checkbox"
label="Enable MikroTik API"
label={t("Enable MikroTik API")}
/>
</div>
@ -658,8 +658,8 @@
<div class="col-span-full">
<.input
field={@form[:mikrotik_username]}
label="Username"
placeholder="Leave blank to inherit from site/organization"
label={t("Username")}
placeholder={t("Leave blank to inherit from site/organization")}
/>
</div>
@ -667,8 +667,8 @@
<.input
field={@form[:mikrotik_password]}
type="password"
label="Password"
placeholder="Leave blank to inherit from site/organization"
label={t("Password")}
placeholder={t("Leave blank to inherit from site/organization")}
/>
</div>
@ -676,7 +676,7 @@
<.input
field={@form[:mikrotik_port]}
type="number"
label="API Port"
label={t("API Port")}
placeholder="8729 (SSL) or 8728 (plain)"
/>
</div>
@ -685,7 +685,7 @@
<.input
field={@form[:mikrotik_use_ssl]}
type="checkbox"
label="Use SSL (API-SSL)"
label={t("Use SSL (API-SSL)")}
/>
</div>
@ -694,8 +694,8 @@
<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>
Plain API (port 8728) sends credentials unencrypted over the network.
<strong>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>
@ -711,13 +711,13 @@
<!-- Save/Cancel Footer -->
<div class="flex items-center justify-between gap-x-6 border-t border-gray-200 px-4 py-4 sm:px-8 dark:border-white/10">
<.button navigate={~p"/devices"}>Cancel</.button>
<.button navigate={~p"/devices"}>{t("Cancel")}</.button>
<.button
phx-disable-with="Saving..."
phx-disable-with={t("Saving...")}
variant="primary"
disabled={@duplicate_device != nil or @non_routable_ip_error}
>
Save Device
{t("Save Device")}
</.button>
</div>
</.form>
@ -726,20 +726,20 @@
<%= 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">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">
Permanently delete this device and all its data.
{t("Permanently delete this device and all its data.")}
</p>
</div>
<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">
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="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

@ -7,7 +7,7 @@
<div>
<.header>
{@page_title}
<:subtitle>Monitor and manage all your network devices</:subtitle>
<:subtitle>{t("Monitor and manage all your network devices")}</:subtitle>
</.header>
</div>
@ -57,7 +57,7 @@
<.button
type="button"
phx-click="reset_order"
data-confirm="Reset all sites and devices to alphabetical order?"
data-confirm={t("Reset all sites and devices to alphabetical order?")}
>
<.icon name="hero-arrow-path" class="h-4 w-4" /> Reset Order
</.button>
@ -97,7 +97,7 @@
<.button
type="button"
phx-click="force_rediscover_all"
data-confirm="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>
@ -118,7 +118,7 @@
end
]}
>
Existing Devices
{t("Existing Devices")}
</.link>
<.link
@ -132,7 +132,7 @@
end
]}
>
Discovered Devices
{t("Discovered Devices")}
<%= if @discovered_devices != [] do %>
<span class="ml-2 inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200">
{length(@discovered_devices)}
@ -152,11 +152,11 @@
</div>
<div class="ml-3 flex-1">
<h3 class="text-sm font-medium text-blue-800 dark:text-blue-200">
Organize your devices with sites
{t("Organize your devices with sites")}
</h3>
<div class="mt-2 text-sm text-blue-700 dark:text-blue-300">
<p>
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,9 +172,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">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">
Get started by adding your first device.
{t("Get started by adding your first device.")}
</p>
<div class="mt-6">
<.button navigate={~p"/devices/new"} variant="primary">
@ -230,37 +230,37 @@
!@reorder_mode && "pl-4"
]}
>
Name
{t("Name")}
</th>
<th
scope="col"
class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 dark:text-white"
>
IP Address
{t("IP Address")}
</th>
<th
scope="col"
class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 dark:text-white"
>
Status
{t("Status")}
</th>
<th
scope="col"
class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 dark:text-white"
>
Last Seen
{t("Last Seen")}
</th>
<th
scope="col"
class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 dark:text-white"
>
Response
{t("Response")}
</th>
<th
scope="col"
class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 dark:text-white"
>
Subscribers
{t("Subscribers")}
</th>
</tr>
</thead>
@ -495,10 +495,10 @@
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">
No discovered devices
{t("No discovered devices")}
</h3>
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">
Devices will appear here as they are discovered via LLDP/CDP.
{t("Devices will appear here as they are discovered via LLDP/CDP.")}
</p>
</div>
<% else %>
@ -513,12 +513,12 @@
id="discovered-devices"
rows={@discovered_devices}
>
<:col :let={discovered} label="Identifier">
<:col :let={discovered} label={t("Identifier")}>
<%= case discovered.identifier.type do %>
<% :mac -> %>
<div>
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200">
MAC
{t("MAC")}
</span>
<span class="ml-2 font-mono text-sm text-gray-900 dark:text-white">
{discovered.identifier.value}
@ -527,7 +527,7 @@
<% :ip -> %>
<div>
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200">
IP
{t("IP")}
</span>
<span class="ml-2 font-mono text-sm text-gray-900 dark:text-white">
{discovered.identifier.value}
@ -536,7 +536,7 @@
<% :hostname -> %>
<div>
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200">
HOST
{t("HOST")}
</span>
<span class="ml-2 text-sm text-gray-900 dark:text-white">
{discovered.identifier.value}
@ -547,7 +547,7 @@
<% end %>
</:col>
<:col :let={discovered} label="Hostname">
<:col :let={discovered} label={t("Hostname")}>
<%= if discovered.hostname do %>
<span class="text-sm font-medium text-gray-900 dark:text-white">
{discovered.hostname}
@ -557,7 +557,7 @@
<% end %>
</:col>
<:col :let={discovered} label="Type">
<:col :let={discovered} label={t("Type")}>
<div class="flex items-center gap-2">
<.icon
name={device_type_icon(discovered.device_type)}
@ -569,7 +569,7 @@
</div>
</:col>
<:col :let={discovered} label="Manufacturer">
<:col :let={discovered} label={t("Manufacturer")}>
<%= if discovered.manufacturer do %>
<span class="text-sm text-gray-900 dark:text-white">
{discovered.manufacturer}
@ -579,7 +579,7 @@
<% end %>
</:col>
<:col :let={discovered} label="Discovered By">
<:col :let={discovered} label={t("Discovered By")}>
<div class="text-sm text-gray-600 dark:text-gray-400">
<%= if length(discovered.discovered_by) == 1 do %>
<% [first] = discovered.discovered_by %>
@ -596,14 +596,14 @@
</div>
</:col>
<:col :let={discovered} label="Last Seen">
<:col :let={discovered} label={t("Last Seen")}>
<.timestamp
datetime={discovered.last_seen}
class="text-sm text-gray-600 dark:text-gray-400"
/>
</:col>
<:col :let={discovered} label="Actions">
<:col :let={discovered} label={t("Actions")}>
<.button
phx-click="add_discovered_device"
phx-value-identifier={Jason.encode!(discovered.identifier)}
@ -622,20 +622,20 @@
patch={~p"/devices?tab=discovered&page=#{@pagination.page - 1}"}
class="relative inline-flex items-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 dark:border-white/10 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700"
>
Previous
{t("Previous")}
</.link>
<.link
:if={@pagination.page < @pagination.total_pages}
patch={~p"/devices?tab=discovered&page=#{@pagination.page + 1}"}
class="relative ml-3 inline-flex items-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 dark:border-white/10 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700"
>
Next
{t("Next")}
</.link>
</div>
<div class="hidden sm:flex sm:flex-1 sm:items-center sm:justify-between">
<div>
<p class="text-sm text-gray-700 dark:text-gray-400">
Showing
{t("Showing")}
<span class="font-medium">
{(@pagination.page - 1) * @pagination.per_page + 1}
</span>
@ -650,7 +650,7 @@
<div>
<nav
class="isolate inline-flex -space-x-px rounded-md shadow-sm"
aria-label="Pagination"
aria-label={t("Pagination")}
>
<.link
:if={@pagination.page > 1}

View file

@ -228,7 +228,7 @@ defmodule ToweropsWeb.DeviceLive.Show do
{:noreply,
socket
|> assign(:show_check_form, false)
|> put_flash(:info, "Check created successfully")
|> put_flash(:info, t("Check created successfully"))
|> load_equipment_data(socket.assigns.device.id)}
end
@ -1280,10 +1280,10 @@ defmodule ToweropsWeb.DeviceLive.Show do
# Queue discovery job
case %{device_id: device.id} |> DiscoveryWorker.new() |> Oban.insert() do
{:ok, _job} ->
{:noreply, put_flash(socket, :info, "Discovery started for #{device.name}")}
{:noreply, put_flash(socket, :info, t("Discovery started for %{name}", name: device.name))}
{:error, _} ->
{:noreply, put_flash(socket, :error, "Failed to start discovery. Please try again.")}
{:noreply, put_flash(socket, :error, t("Failed to start discovery. Please try again."))}
end
end
@ -1353,7 +1353,7 @@ defmodule ToweropsWeb.DeviceLive.Show do
{:noreply, assign_preseem_data(socket)}
{:error, _} ->
{:noreply, put_flash(socket, :error, "Failed to dismiss insight")}
{:noreply, put_flash(socket, :error, t("Failed to dismiss insight"))}
end
end

View file

@ -57,7 +57,7 @@
</span>
<span class="text-gray-300 dark:text-gray-600">•</span>
<span class="text-xs text-gray-500 dark:text-gray-400">
Polled by:
{t("Polled by:")}
</span>
<%= if @agent_info.type == :cloud do %>
<span class="inline-flex items-center gap-1 text-xs text-blue-600 dark:text-blue-400">
@ -65,7 +65,7 @@
<%= if @agent_info.agent_token_id do %>
Cloud ({@agent_info.name})
<% else %>
Cloud
{t("Cloud")}
<% end %>
</span>
<% else %>
@ -104,7 +104,7 @@
end
]}
>
Overview
{t("Overview")}
</.link>
<%= if @snmp_device do %>
@ -119,7 +119,7 @@
end
]}
>
Ports
{t("Ports")}
</.link>
<% end %>
@ -134,7 +134,7 @@
end
]}
>
Neighbors
{t("Neighbors")}
</.link>
<%= if @arp_entries && length(@arp_entries) > 0 do %>
@ -149,7 +149,7 @@
end
]}
>
ARP Table
{t("ARP Table")}
</.link>
<% end %>
@ -165,7 +165,7 @@
end
]}
>
MAC Table
{t("MAC Table")}
</.link>
<% end %>
@ -181,7 +181,7 @@
end
]}
>
VLANs
{t("VLANs")}
</.link>
<% end %>
@ -197,7 +197,7 @@
end
]}
>
IP Addresses
{t("IP Addresses")}
</.link>
<% end %>
@ -215,7 +215,7 @@
>
<span class="flex items-center gap-1.5">
<.icon name="hero-beaker" class="h-3.5 w-3.5 text-blue-500 dark:text-blue-400" />
Backups
{t("Backups")}
</span>
</.link>
<% end %>
@ -227,7 +227,7 @@
>
<span class="flex items-center gap-1.5">
<.icon name="hero-clock" class="h-3.5 w-3.5 text-orange-500 dark:text-orange-400" />
Config Timeline
{t("Config Timeline")}
</span>
</.link>
<% end %>
@ -246,7 +246,7 @@
>
<span class="flex items-center gap-1.5">
<.icon name="hero-signal" class="h-3.5 w-3.5 text-purple-500 dark:text-purple-400" />
Preseem
{t("Preseem")}
</span>
</.link>
<% end %>
@ -283,7 +283,7 @@
end
]}
>
Checks
{t("Checks")}
</.link>
<.link
@ -297,7 +297,7 @@
end
]}
>
Events
{t("Events")}
</.link>
<%= if @snmp_device && @snmp_device.raw_discovery_data do %>
@ -312,7 +312,7 @@
end
]}
>
Raw Data
{t("Raw Data")}
</.link>
<% end %>
</nav>
@ -329,7 +329,7 @@
<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">
Device Information
{t("Device Information")}
</h3>
</div>
<div class="p-4">
@ -389,7 +389,7 @@
/>
<div class="flex-1 min-w-0">
<h4 class="text-sm font-semibold text-gray-900 dark:text-white mb-2">
Firmware Update Available
{t("Firmware Update Available")}
</h4>
<div class="space-y-1.5 text-xs text-gray-600 dark:text-gray-400">
<div>
@ -419,7 +419,7 @@
rel="noopener noreferrer"
class="inline-flex items-center gap-1 rounded px-2 py-1 text-xs font-medium bg-blue-600 text-white hover:bg-blue-700"
>
Download
{t("Download")}
<.icon
name="hero-arrow-top-right-on-square"
class="h-3 w-3"
@ -432,7 +432,7 @@
rel="noopener noreferrer"
class="inline-flex items-center gap-1 rounded px-2 py-1 text-xs font-medium text-blue-600 dark:text-blue-400 hover:bg-blue-50 dark:hover:bg-blue-900/20"
>
Release Notes
{t("Release Notes")}
<.icon
name="hero-arrow-top-right-on-square"
class="h-3 w-3"
@ -542,7 +542,7 @@
>
<div class="flex items-center justify-between">
<h3 class="text-sm font-semibold text-gray-900 dark:text-white">
Overall Traffic
{t("Overall Traffic")}
</h3>
<.icon name="hero-arrow-right" class="h-4 w-4 text-gray-400" />
</div>
@ -571,7 +571,7 @@
>
<div class="flex items-center justify-between">
<h3 class="text-sm font-semibold text-gray-900 dark:text-white">
ICMP Latency
{t("ICMP Latency")}
</h3>
<.icon name="hero-arrow-right" class="h-4 w-4 text-gray-400" />
</div>
@ -597,7 +597,7 @@
<div class="px-4 py-3 border-b border-gray-200 dark:border-white/10 flex items-center justify-between">
<h3 class="text-sm font-semibold text-gray-900 dark:text-white flex items-center gap-1.5">
<.icon name="hero-clock" class="h-4 w-4 text-orange-500" />
Recent Config Changes
{t("Recent Config Changes")}
</h3>
<.link
navigate={~p"/devices/#{@device.id}/config-timeline"}
@ -653,7 +653,7 @@
do: "Processor",
else: "Processors"}
<% else %>
Processors
{t("Processors")}
<% end %>
</h3>
<.icon name="hero-arrow-right" class="h-4 w-4 text-gray-400" />
@ -681,7 +681,7 @@
]}>
<div class="flex items-center justify-between gap-4">
<span class="text-sm text-gray-700 dark:text-gray-300">
Average Load
{t("Average Load")}
</span>
<div class="flex items-center gap-3 flex-1 max-w-md">
<div class="flex-1 bg-gray-200 rounded-full h-3 dark:bg-gray-700">
@ -714,7 +714,7 @@
>
<div class="flex items-center justify-between">
<h3 class="text-sm font-semibold text-gray-900 dark:text-white">
Memory Usage
{t("Memory Usage")}
</h3>
<.icon name="hero-arrow-right" class="h-4 w-4 text-gray-400" />
</div>
@ -736,7 +736,7 @@
<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">
Storage Usage
{t("Storage Usage")}
</h3>
</div>
<div class="p-4">
@ -761,7 +761,7 @@
)}
{sensor.sensor_unit}
<% else %>
N/A
{t("N/A")}
<% end %>
</dd>
</div>
@ -775,7 +775,7 @@
<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">
Storage Volumes
{t("Storage Volumes")}
<span class="ml-2 text-xs font-normal text-gray-500 dark:text-gray-400">
({length(@storage)})
</span>
@ -848,7 +848,7 @@
<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">
Temperature
{t("Temperature")}
</h3>
</div>
<div class="p-4">
@ -873,7 +873,7 @@
)}
{sensor.sensor_unit || "°C"}
<% else %>
N/A
{t("N/A")}
<% end %>
</dd>
</div>
@ -886,7 +886,7 @@
<%= 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">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">
@ -910,7 +910,7 @@
)}
{sensor.sensor_unit}
<% else %>
N/A
{t("N/A")}
<% end %>
</dd>
</div>
@ -924,7 +924,7 @@
<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">
Transceivers
{t("Transceivers")}
</h3>
</div>
<div class="p-4">
@ -950,7 +950,7 @@
)}
{sensor.sensor_unit}
<% else %>
N/A
{t("N/A")}
<% end %>
</dd>
</div>
@ -964,7 +964,7 @@
<%= 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">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 -->
@ -989,7 +989,7 @@
{sensor.sensor_unit}
<% end %>
<% else %>
N/A
{t("N/A")}
<% end %>
</dd>
</div>
@ -1000,7 +1000,7 @@
<%= if @firewall_counters && length(@firewall_counters) > 0 do %>
<div>
<h4 class="text-xs font-semibold text-gray-700 dark:text-gray-300 mb-2 uppercase tracking-wider">
Firewall
{t("Firewall")}
</h4>
<dl class="space-y-1">
<%= for sensor <- @firewall_counters do %>
@ -1022,7 +1022,7 @@
{sensor.sensor_unit}
<% end %>
<% else %>
N/A
{t("N/A")}
<% end %>
</dd>
</div>
@ -1037,7 +1037,7 @@
<%= 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">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">
@ -1064,7 +1064,7 @@
{sensor.sensor_unit}
<% end %>
<% else %>
N/A
{t("N/A")}
<% end %>
</dd>
</div>
@ -1080,7 +1080,7 @@
<div class="mt-6 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">
Connected Devices
{t("Connected Devices")}
<span class="ml-2 text-xs font-normal text-gray-500 dark:text-gray-400">
({length(@connected_devices)} links)
</span>
@ -1189,11 +1189,11 @@
<td class="px-4 py-3">
<%= if peer_device do %>
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200">
Managed
{t("Managed")}
</span>
<% else %>
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-300">
Discovered
{t("Discovered")}
</span>
<% end %>
</td>
@ -1307,7 +1307,7 @@
<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">
Discovered Neighbors
{t("Discovered Neighbors")}
</h3>
</div>
<div class="overflow-x-auto">
@ -1361,7 +1361,7 @@
<%= if neighbor.remote_system_name do %>
{neighbor.remote_system_name |> String.split("/") |> List.first()}
<% else %>
Unknown
{t("Unknown")}
<% end %>
</div>
<% end %>
@ -1403,10 +1403,10 @@
<div class="text-center">
<.icon name="hero-server-stack" class="mx-auto h-12 w-12 text-gray-400" />
<h3 class="mt-2 text-sm font-semibold text-gray-900 dark:text-white">
No neighbors discovered
{t("No neighbors discovered")}
</h3>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
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>
@ -1416,7 +1416,7 @@
<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">
ARP Table
{t("ARP Table")}
<span class="ml-2 text-xs font-normal text-gray-500 dark:text-gray-400">
({length(@arp_entries)} entries)
</span>
@ -1483,10 +1483,10 @@
<div class="text-center">
<.icon name="hero-table-cells" class="mx-auto h-12 w-12 text-gray-400" />
<h3 class="mt-2 text-sm font-semibold text-gray-900 dark:text-white">
No ARP entries discovered
{t("No ARP entries discovered")}
</h3>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
This device doesn't have any ARP entries, or ARP discovery hasn't run yet.
{t("This device doesn't have any ARP entries, or ARP discovery hasn't run yet.")}
</p>
</div>
</div>
@ -1496,7 +1496,7 @@
<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">
MAC Address Table
{t("MAC Address Table")}
<span class="ml-2 text-xs font-normal text-gray-500 dark:text-gray-400">
({length(@mac_addresses)} entries)
</span>
@ -1569,10 +1569,10 @@
<div class="text-center">
<.icon name="hero-table-cells" class="mx-auto h-12 w-12 text-gray-400" />
<h3 class="mt-2 text-sm font-semibold text-gray-900 dark:text-white">
No MAC addresses discovered
{t("No MAC addresses discovered")}
</h3>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
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>
@ -1582,7 +1582,7 @@
<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">
Configured VLANs
{t("Configured VLANs")}
<span class="ml-2 text-xs font-normal text-gray-500 dark:text-gray-400">
({length(@vlans)})
</span>
@ -1638,10 +1638,10 @@
<div class="text-center">
<.icon name="hero-square-3-stack-3d" class="mx-auto h-12 w-12 text-gray-400" />
<h3 class="mt-2 text-sm font-semibold text-gray-900 dark:text-white">
No VLANs discovered
{t("No VLANs discovered")}
</h3>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
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>
@ -1654,9 +1654,9 @@
<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">
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200 mr-2">
IPv4
{t("IPv4")}
</span>
Addresses
{t("Addresses")}
<span class="ml-2 text-xs font-normal text-gray-500 dark:text-gray-400">
({length(@ipv4_addresses)})
</span>
@ -1692,7 +1692,7 @@
<td class="px-4 py-3">
<%= if ip.is_primary do %>
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200">
Primary
{t("Primary")}
</span>
<% else %>
<span class="text-gray-400 dark:text-gray-500">-</span>
@ -1711,9 +1711,9 @@
<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">
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200 mr-2">
IPv6
{t("IPv6")}
</span>
Addresses
{t("Addresses")}
<span class="ml-2 text-xs font-normal text-gray-500 dark:text-gray-400">
({length(@ipv6_addresses)})
</span>
@ -1754,10 +1754,10 @@
<div class="text-center">
<.icon name="hero-globe-alt" class="mx-auto h-12 w-12 text-gray-400" />
<h3 class="mt-2 text-sm font-semibold text-gray-900 dark:text-white">
No IP addresses discovered
{t("No IP addresses discovered")}
</h3>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
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>
@ -1770,7 +1770,7 @@
<div class="flex items-center justify-between">
<div class="flex items-center gap-2">
<h3 class="text-sm font-semibold text-gray-900 dark:text-white">
Configuration Backups
{t("Configuration Backups")}
<%= if Enum.any?(@mikrotik_backups) do %>
<span class="ml-2 text-xs font-normal text-gray-500 dark:text-gray-400">
({length(@mikrotik_backups)})
@ -1791,7 +1791,7 @@
</button>
<% else %>
<div class="text-sm text-amber-600 dark:text-amber-400">
Assign an agent to enable backups
{t("Assign an agent to enable backups")}
</div>
<% end %>
</div>
@ -1807,7 +1807,7 @@
/>
<div class="text-sm text-blue-900 dark:text-blue-200">
<span class="font-medium">Compare configurations:</span>
Select any 2 backups using the checkboxes to compare their differences
{t("Select any 2 backups using the checkboxes to compare their differences")}
</div>
</div>
</div>
@ -1834,7 +1834,7 @@
<%= for backup <- @mikrotik_backups do %>
<% compression_ratio =
if backup.config_size_bytes && backup.config_size_bytes > 0 do
Float.round(
{t("Float.round(")}
(1 - backup.compressed_size_bytes / backup.config_size_bytes) * 100,
1
)
@ -1903,7 +1903,7 @@
<button
phx-click="delete_backup"
phx-value-id={backup.id}
data-confirm="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
@ -1945,7 +1945,7 @@
<%!-- Instructions when only 1 selected --%>
<%= if MapSet.size(@selected_backup_ids) == 1 do %>
<div class="text-xs text-gray-500 dark:text-gray-400 pl-7">
Select one more backup to compare
{t("Select one more backup to compare")}
</div>
<% end %>
<%!-- Action buttons --%>
@ -1954,7 +1954,7 @@
phx-click="clear_selection"
class="inline-flex items-center gap-1.5 px-3 py-2 text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 border border-gray-300 rounded-lg transition-colors dark:bg-gray-700 dark:text-gray-300 dark:border-gray-600 dark:hover:bg-gray-600"
>
Clear
{t("Clear")}
</button>
<button
phx-click="compare_selected"
@ -1975,7 +1975,7 @@
<div class="p-8 text-center">
<.icon name="hero-document-text" class="mx-auto h-12 w-12 text-gray-400" />
<h3 class="mt-2 text-sm font-semibold text-gray-900 dark:text-white">
No backups yet
{t("No backups yet")}
</h3>
<%= if @agent_info.agent_token_id do %>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
@ -1986,7 +1986,7 @@
</p>
<% else %>
<p class="mt-1 text-sm text-amber-600 dark:text-amber-400">
Assign an agent to enable automatic backups.
{t("Assign an agent to enable automatic backups.")}
</p>
<% end %>
</div>
@ -1997,14 +1997,14 @@
<div class="text-center">
<.icon name="hero-server" class="mx-auto h-12 w-12 text-gray-400" />
<h3 class="mt-2 text-sm font-semibold text-gray-900 dark:text-white">
MikroTik API not enabled
{t("MikroTik API not enabled")}
</h3>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
Enable MikroTik API in device settings to enable configuration backups.
{t("Enable MikroTik API in device settings to enable configuration backups.")}
</p>
<div class="mt-6">
<.button navigate={~p"/devices/#{@device.id}/edit"}>
Edit Device Settings
{t("Edit Device Settings")}
</.button>
</div>
</div>
@ -2013,9 +2013,9 @@
<% "logs" -> %>
<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">Event Logs</h3>
<h3 class="text-sm font-semibold text-gray-900 dark:text-white">{t("Event Logs")}</h3>
<p class="text-xs text-gray-600 dark:text-gray-400 mt-1">
Showing the most recent 100 events
{t("Showing the most recent 100 events")}
</p>
</div>
<%= if Enum.any?(@events) do %>
@ -2066,10 +2066,10 @@
<div class="p-8 text-center">
<.icon name="hero-document-text" class="mx-auto h-12 w-12 text-gray-400" />
<h3 class="mt-2 text-sm font-semibold text-gray-900 dark:text-white">
No events yet
{t("No events yet")}
</h3>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
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 %>
@ -2078,11 +2078,11 @@
<div class="bg-white dark:bg-gray-800/50 shadow sm:rounded-lg">
<div class="px-4 py-5 sm:p-6">
<h3 class="text-lg font-medium leading-6 text-gray-900 dark:text-white">
Raw Discovery Data
{t("Raw Discovery Data")}
</h3>
<div class="mt-2 max-w-xl text-sm text-gray-500 dark:text-gray-400">
<p>
Raw SNMP OID values collected during the last discovery run at
{t("Raw SNMP OID values collected during the last discovery run at")}
<%= if @snmp_device.last_discovery_at do %>
{ToweropsWeb.TimeHelpers.format_iso8601(
@snmp_device.last_discovery_at,
@ -2096,20 +2096,20 @@
<%= if @snmp_device.raw_discovery_data do %>
<div class="mt-5">
<h4 class="text-sm font-medium text-gray-900 dark:text-white mb-3">
System OIDs
{t("System OIDs")}
</h4>
<div class="overflow-hidden rounded-lg border border-gray-200 dark:border-white/10">
<table class="min-w-full divide-y divide-gray-200 dark:divide-white/10">
<thead class="bg-gray-50 dark:bg-gray-800/75">
<tr>
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Field
{t("Field")}
</th>
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
OID
{t("OID")}
</th>
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Value
{t("Value")}
</th>
</tr>
</thead>
@ -2133,17 +2133,17 @@
<%= if Map.get(@snmp_device.raw_discovery_data, "health_sensor_table") do %>
<h4 class="text-sm font-medium text-gray-900 dark:text-white mt-6 mb-3">
Health Sensor Table Walk
{t("Health Sensor Table Walk")}
</h4>
<div class="overflow-hidden rounded-lg border border-gray-200 dark:border-white/10">
<table class="min-w-full divide-y divide-gray-200 dark:divide-white/10">
<thead class="bg-gray-50 dark:bg-gray-800/75">
<tr>
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
OID
{t("OID")}
</th>
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Value
{t("Value")}
</th>
</tr>
</thead>
@ -2165,26 +2165,26 @@
<%= if Map.get(@snmp_device.raw_discovery_data, "discovered_sensors") do %>
<h4 class="text-sm font-medium text-gray-900 dark:text-white mt-6 mb-3">
Discovered Sensors
{t("Discovered Sensors")}
</h4>
<div class="overflow-hidden rounded-lg border border-gray-200 dark:border-white/10">
<table class="min-w-full divide-y divide-gray-200 dark:divide-white/10">
<thead class="bg-gray-50 dark:bg-gray-800/75">
<tr>
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Type
{t("Type")}
</th>
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Description
{t("Description")}
</th>
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
OID
{t("OID")}
</th>
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Value
{t("Value")}
</th>
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
State
{t("State")}
</th>
</tr>
</thead>
@ -2225,7 +2225,7 @@
<% end %>
<h4 class="text-sm font-medium text-gray-900 dark:text-white mt-6 mb-3">
Copyable Raw Data
{t("Copyable Raw Data")}
</h4>
<div class="rounded-lg border border-gray-200 dark:border-white/10">
<textarea
@ -2242,7 +2242,7 @@
<% 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">
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 %>
@ -2255,10 +2255,10 @@
<div class="px-4 py-12 text-center">
<.icon name="hero-clipboard-document-check" class="mx-auto h-12 w-12 text-gray-400" />
<h3 class="mt-2 text-sm font-semibold text-gray-900 dark:text-white">
No checks configured
{t("No checks configured")}
</h3>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
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 %>
@ -2301,25 +2301,25 @@
scope="col"
class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider"
>
Name
{t("Name")}
</th>
<th
scope="col"
class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider"
>
Status
{t("Status")}
</th>
<th
scope="col"
class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider"
>
Value
{t("Value")}
</th>
<th
scope="col"
class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider"
>
Last Checked
{t("Last Checked")}
</th>
<th scope="col" class="relative px-4 py-3">
<span class="sr-only">Actions</span>
@ -2356,7 +2356,7 @@
}
class="text-blue-600 hover:text-blue-900 dark:text-blue-400 dark:hover:text-blue-300 text-sm font-medium"
>
Graph
{t("Graph")}
</.link>
</td>
</tr>
@ -2375,7 +2375,7 @@
<div class="bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10 overflow-hidden">
<div class="px-4 py-3 border-b border-gray-200 dark:border-white/10 bg-gray-50 dark:bg-white/5">
<h3 class="text-sm font-medium text-gray-900 dark:text-white">
Gaiia Inventory Item
{t("Gaiia Inventory Item")}
</h3>
</div>
<div class="px-4 py-4">
@ -2423,7 +2423,7 @@
<%= if @gaiia_item.manufacturer_name do %>
<div>
<dt class="text-xs font-medium text-gray-500 dark:text-gray-400">
Manufacturer
{t("Manufacturer")}
</dt>
<dd class="mt-1 text-sm text-gray-900 dark:text-white">
{@gaiia_item.manufacturer_name}
@ -2433,7 +2433,7 @@
<%= if @gaiia_item.category do %>
<div>
<dt class="text-xs font-medium text-gray-500 dark:text-gray-400">
Category
{t("Category")}
</dt>
<dd class="mt-1 text-sm text-gray-900 dark:text-white">
{@gaiia_item.category}
@ -2443,7 +2443,7 @@
<%= if @gaiia_item.serial_number do %>
<div>
<dt class="text-xs font-medium text-gray-500 dark:text-gray-400">
Serial Number
{t("Serial Number")}
</dt>
<dd class="mt-1 text-sm font-mono text-gray-900 dark:text-white">
{@gaiia_item.serial_number}
@ -2453,7 +2453,7 @@
<%= if @gaiia_item.mac_address do %>
<div>
<dt class="text-xs font-medium text-gray-500 dark:text-gray-400">
MAC Address
{t("MAC Address")}
</dt>
<dd class="mt-1 text-sm font-mono text-gray-900 dark:text-white">
{@gaiia_item.mac_address}
@ -2463,14 +2463,14 @@
<%= if @gaiia_item.ip_address do %>
<div>
<dt class="text-xs font-medium text-gray-500 dark:text-gray-400">
IP Address (Gaiia)
{t("IP Address (Gaiia)")}
</dt>
<dd class="mt-1 text-sm font-mono text-gray-900 dark:text-white">
{@gaiia_item.ip_address}
<%= if @device.ip_address && @gaiia_item.ip_address != @device.ip_address do %>
<span class="ml-2 inline-flex items-center rounded-full bg-amber-100 px-2 py-0.5 text-xs font-medium text-amber-800 dark:bg-amber-900/30 dark:text-amber-400">
<.icon name="hero-exclamation-triangle-mini" class="mr-0.5 h-3 w-3" />
Mismatch
{t("Mismatch")}
</span>
<% end %>
</dd>
@ -2485,14 +2485,14 @@
<div class="bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10 overflow-hidden">
<div class="px-4 py-3 border-b border-gray-200 dark:border-white/10 bg-gray-50 dark:bg-white/5">
<h3 class="text-sm font-medium text-gray-900 dark:text-white">
Gaiia Network Site
{t("Gaiia Network Site")}
</h3>
</div>
<div class="px-4 py-4">
<dl class="grid grid-cols-1 gap-x-4 gap-y-4 sm:grid-cols-2 lg:grid-cols-3">
<div>
<dt class="text-xs font-medium text-gray-500 dark:text-gray-400">
Site Name
{t("Site Name")}
</dt>
<dd class="mt-1 text-sm text-gray-900 dark:text-white">
{@gaiia_network_site.name}
@ -2501,7 +2501,7 @@
<%= if @gaiia_network_site.account_count do %>
<div>
<dt class="text-xs font-medium text-gray-500 dark:text-gray-400">
Subscribers
{t("Subscribers")}
</dt>
<dd class="mt-1 text-sm text-gray-900 dark:text-white">
{@gaiia_network_site.account_count}
@ -2511,7 +2511,7 @@
<%= if @gaiia_network_site.total_mrr do %>
<div>
<dt class="text-xs font-medium text-gray-500 dark:text-gray-400">
Monthly Revenue
{t("Monthly Revenue")}
</dt>
<dd class="mt-1 text-sm text-gray-900 dark:text-white">
${Decimal.round(@gaiia_network_site.total_mrr, 2)}
@ -2528,7 +2528,7 @@
<div class="bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10 overflow-hidden">
<div class="px-4 py-3 border-b border-gray-200 dark:border-white/10 bg-gray-50 dark:bg-white/5">
<h3 class="text-sm font-medium text-gray-900 dark:text-white">
Linked Subscriber
{t("Linked Subscriber")}
</h3>
</div>
<div class="px-4 py-4">
@ -2542,7 +2542,7 @@
<%= if @gaiia_account.readable_id do %>
<div>
<dt class="text-xs font-medium text-gray-500 dark:text-gray-400">
Account ID
{t("Account ID")}
</dt>
<dd class="mt-1 text-sm font-mono text-gray-900 dark:text-white">
{@gaiia_account.readable_id}
@ -2587,7 +2587,7 @@
<div class="bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10 overflow-hidden">
<div class="px-4 py-3 border-b border-gray-200 dark:border-white/10 bg-gray-50 dark:bg-white/5">
<h3 class="text-sm font-medium text-gray-900 dark:text-white">
Billing Subscriptions
{t("Billing Subscriptions")}
</h3>
</div>
<div class="overflow-x-auto">
@ -2598,19 +2598,19 @@
scope="col"
class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400"
>
Plan
{t("Plan")}
</th>
<th
scope="col"
class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400"
>
Status
{t("Status")}
</th>
<th
scope="col"
class="px-4 py-3 text-right text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400"
>
MRR
{t("MRR")}
</th>
</tr>
</thead>
@ -2658,7 +2658,7 @@
<% else %>
<div class="bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10 p-8 text-center">
<p class="text-sm text-gray-500 dark:text-gray-400">
No Gaiia inventory item linked to this device.
{t("No Gaiia inventory item linked to this device.")}
</p>
</div>
<% end %>
@ -2734,7 +2734,7 @@
</div>
<div class="bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10 p-4">
<div class="text-sm font-medium text-gray-500 dark:text-gray-400">
Capacity Score
{t("Capacity Score")}
</div>
<div class="mt-1 text-2xl font-semibold text-gray-900 dark:text-white">
{if @preseem_access_point.capacity_score,
@ -2756,7 +2756,7 @@
<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">
Access Point Details
{t("Access Point Details")}
</h3>
</div>
<div class="px-4 py-3">
@ -2769,7 +2769,7 @@
</div>
<div>
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">
Preseem ID
{t("Preseem ID")}
</dt>
<dd class="mt-0.5 text-sm text-gray-900 dark:text-white">
{@preseem_access_point.preseem_id}
@ -2777,7 +2777,7 @@
</div>
<div>
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">
Subscribers
{t("Subscribers")}
</dt>
<dd class="mt-0.5 text-sm text-gray-900 dark:text-white">
{@preseem_access_point.subscriber_count || "---"}
@ -2785,7 +2785,7 @@
</div>
<div>
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">
Busy Hours
{t("Busy Hours")}
</dt>
<dd class="mt-0.5 text-sm text-gray-900 dark:text-white">
<%= if @preseem_access_point.busy_hours do %>
@ -2797,7 +2797,7 @@
</div>
<div>
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">
Airtime Utilization
{t("Airtime Utilization")}
</dt>
<dd class="mt-0.5 text-sm text-gray-900 dark:text-white">
<%= if @preseem_access_point.airtime_utilization do %>
@ -2809,7 +2809,7 @@
</div>
<div>
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">
Match Type
{t("Match Type")}
</dt>
<dd class="mt-0.5 text-sm text-gray-900 dark:text-white">
{@preseem_access_point.match_confidence}
@ -2824,7 +2824,7 @@
<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">
Recent QoE Metrics
{t("Recent QoE Metrics")}
</h3>
</div>
<div class="overflow-x-auto">
@ -2832,25 +2832,25 @@
<thead>
<tr>
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">
Time
{t("Time")}
</th>
<th class="px-4 py-2 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">
Latency (ms)
{t("Latency (ms)")}
</th>
<th class="px-4 py-2 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">
P95 Latency
{t("P95 Latency")}
</th>
<th class="px-4 py-2 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">
Jitter (ms)
{t("Jitter (ms)")}
</th>
<th class="px-4 py-2 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">
Loss (%)
{t("Loss (%)")}
</th>
<th class="px-4 py-2 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">
Throughput
{t("Throughput")}
</th>
<th class="px-4 py-2 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">
Subscribers
{t("Subscribers")}
</th>
</tr>
</thead>
@ -2901,7 +2901,7 @@
<% else %>
<div class="bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10 p-8 text-center">
<p class="text-sm text-gray-500 dark:text-gray-400">
No Preseem data linked to this device.
{t("No Preseem data linked to this device.")}
</p>
</div>
<% end %>

View file

@ -31,25 +31,25 @@ defmodule ToweropsWeb.GraphLive.Show do
{:error, :not_found} ->
{:noreply,
socket
|> put_flash(:error, "Device not found")
|> put_flash(:error, t("Device not found"))
|> push_navigate(to: ~p"/devices")}
{:error, :check_not_found} ->
{:noreply,
socket
|> put_flash(:error, "Check not found")
|> put_flash(:error, t("Check not found"))
|> push_navigate(to: ~p"/devices/#{device_id}?tab=checks")}
{:error, :check_device_mismatch} ->
{:noreply,
socket
|> put_flash(:error, "Check does not belong to this device")
|> put_flash(:error, t("Check does not belong to this device"))
|> push_navigate(to: ~p"/devices/#{device_id}?tab=checks")}
{:error, :access_denied} ->
{:noreply,
socket
|> put_flash(:error, "You don't have access to this device")
|> put_flash(:error, t("You don't have access to this device"))
|> push_navigate(to: ~p"/devices")}
end
end
@ -66,13 +66,13 @@ defmodule ToweropsWeb.GraphLive.Show do
{:error, :not_found} ->
{:noreply,
socket
|> put_flash(:error, "Device not found")
|> put_flash(:error, t("Device not found"))
|> push_navigate(to: ~p"/devices")}
{:error, :access_denied} ->
{:noreply,
socket
|> put_flash(:error, "You don't have access to this device")
|> put_flash(:error, t("You don't have access to this device"))
|> push_navigate(to: ~p"/devices")}
end
end
@ -798,7 +798,7 @@ defmodule ToweropsWeb.GraphLive.Show do
case Devices.get_device(device_id) do
nil ->
socket
|> put_flash(:error, "Device no longer exists")
|> put_flash(:error, t("Device no longer exists"))
|> push_navigate(to: ~p"/devices")
device ->
@ -808,7 +808,7 @@ defmodule ToweropsWeb.GraphLive.Show do
poll_and_update_buffer(socket, device)
else
socket
|> put_flash(:error, "Access to device revoked")
|> put_flash(:error, t("Access to device revoked"))
|> push_navigate(to: ~p"/devices")
end
end

View file

@ -99,10 +99,10 @@
<div class="text-center">
<.icon name="hero-chart-bar" class="mx-auto h-12 w-12 text-gray-400" />
<h3 class="mt-2 text-sm font-semibold text-gray-900 dark:text-white">
No sensor data available
{t("No sensor data available")}
</h3>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
This device doesn't have any sensors of this type.
{t("This device doesn't have any sensors of this type.")}
</p>
</div>
</div>

View file

@ -31,7 +31,7 @@ defmodule ToweropsWeb.HelpLive.Index do
socket =
socket
|> assign(:page_title, "Help")
|> assign(:page_title, t("Help"))
|> assign(:current_user, current_user)
|> assign(:is_authenticated, is_authenticated)
|> assign(:current_scope, current_scope)
@ -75,13 +75,13 @@ defmodule ToweropsWeb.HelpLive.Index do
socket
|> assign(:generated_password, password)
|> assign(:password_generating, false)
|> put_flash(:info, "Password generated successfully from random.org")}
|> put_flash(:info, t("Password generated successfully from random.org"))}
{:error, reason} ->
{:noreply,
socket
|> assign(:password_generating, false)
|> put_flash(:error, "Failed to generate password: #{reason}")}
|> put_flash(:error, t("Failed to generate password: %{reason}", reason: reason))}
end
end

View file

@ -14,7 +14,7 @@ defmodule ToweropsWeb.InsightsLive.Index do
{:ok,
socket
|> assign(:organization, org)
|> assign(:page_title, "Insights")
|> assign(:page_title, t("Insights"))
|> assign(:selected_ids, MapSet.new())}
end
@ -62,10 +62,10 @@ defmodule ToweropsWeb.InsightsLive.Index do
{:noreply,
socket
|> load_insights()
|> put_flash(:info, "Insight dismissed")}
|> put_flash(:info, t("Insight dismissed"))}
{:error, _} ->
{:noreply, put_flash(socket, :error, "Failed to dismiss")}
{:noreply, put_flash(socket, :error, t("Failed to dismiss"))}
end
end
@ -108,7 +108,7 @@ defmodule ToweropsWeb.InsightsLive.Index do
|> put_flash(:info, "#{count} insight(s) dismissed")}
{:error, _} ->
{:noreply, put_flash(socket, :error, "Failed to dismiss insights")}
{:noreply, put_flash(socket, :error, t("Failed to dismiss insights"))}
end
end
end

View file

@ -5,10 +5,10 @@
>
<div class="border-b border-gray-200 pb-5 dark:border-white/5">
<h1 class="text-3xl font-semibold tracking-tight text-gray-900 dark:text-white">
Network Insights
{t("Network Insights")}
</h1>
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
Proactive network health observations from all sources.
{t("Proactive network health observations from all sources.")}
</p>
</div>
@ -30,7 +30,7 @@
)
]}
>
Active
{t("Active")}
</.link>
<.link
id="filter-dismissed"
@ -46,7 +46,7 @@
)
]}
>
Dismissed
{t("Dismissed")}
</.link>
</nav>
@ -66,7 +66,7 @@
)
]}
>
All
{t("All")}
</.link>
<.link
:for={source <- ~w(preseem gaiia snmp system)}
@ -102,7 +102,7 @@
)
]}
>
All
{t("All")}
</.link>
<.link
:for={
@ -145,7 +145,7 @@
phx-click="deselect_all"
class="text-sm text-indigo-600 hover:text-indigo-800 dark:text-indigo-400 dark:hover:text-indigo-300"
>
Deselect All
{t("Deselect All")}
</button>
<button
type="button"
@ -153,7 +153,7 @@
phx-click="bulk_dismiss"
class="rounded-md bg-indigo-600 px-3 py-1.5 text-sm font-medium text-white shadow-xs hover:bg-indigo-500 dark:bg-indigo-500 dark:hover:bg-indigo-400"
>
Dismiss Selected
{t("Dismiss Selected")}
</button>
</div>
<% end %>
@ -170,22 +170,22 @@
class="h-12 w-12 text-amber-500 dark:text-amber-400"
/>
</div>
<h3 class="card-title text-gray-900 dark:text-white">No Insights Yet</h3>
<h3 class="card-title text-gray-900 dark:text-white">{t("No Insights Yet")}</h3>
<p class="text-sm text-gray-600 dark:text-gray-400">
<%= cond do %>
<% @filter_status == "dismissed" -> %>
No dismissed insights.
{t("No dismissed insights.")}
<% @filter_source != nil -> %>
No {@filter_source} insights right now.
<% @filter_urgency != nil -> %>
No {@filter_urgency} insights right now.
<% true -> %>
Connect Preseem or enable SNMP monitoring to get started.
{t("Connect Preseem or enable SNMP monitoring to get started.")}
<% end %>
</p>
<div class="card-actions mt-2">
<span class="text-sm text-base-content/50">
Go to your org settings to connect an integration.
{t("Go to your org settings to connect an integration.")}
</span>
</div>
</div>
@ -200,7 +200,7 @@
phx-click="select_all"
class="text-sm text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white"
>
Select All
{t("Select All")}
</button>
</div>
@ -278,7 +278,7 @@
<%= if insight.metadata["device_names"] && length(insight.metadata["device_names"]) > 0 do %>
<div class="mt-2 rounded-md bg-gray-50 p-3 dark:bg-white/5">
<p class="text-xs font-medium text-gray-700 dark:text-gray-300">
Affected devices:
{t("Affected devices:")}
</p>
<div class="mt-1.5 flex flex-wrap gap-1.5">
<%= for {name, idx} <- Enum.with_index(insight.metadata["device_names"]) do %>
@ -311,7 +311,7 @@
<div class="mt-3 rounded-md border border-blue-200 bg-blue-50 p-3 dark:border-blue-800 dark:bg-blue-900/20">
<p class="text-xs font-medium text-blue-800 dark:text-blue-300 mb-2">
<.icon name="hero-light-bulb" class="h-3.5 w-3.5 inline" />
Recommended actions
{t("Recommended actions")}
</p>
<div class="flex flex-wrap gap-2">
<.link
@ -321,7 +321,7 @@
class="inline-flex items-center gap-1 rounded-md bg-blue-600 px-2.5 py-1.5 text-xs font-medium text-white shadow-xs hover:bg-blue-700"
>
<.icon name="hero-arrows-right-left" class="h-3.5 w-3.5" />
Map to Gaiia inventory
{t("Map to Gaiia inventory")}
</.link>
<.link
navigate={
@ -339,7 +339,7 @@
<div class="mt-3 rounded-md border border-amber-200 bg-amber-50 p-3 dark:border-amber-800 dark:bg-amber-900/20">
<p class="text-xs font-medium text-amber-800 dark:text-amber-300 mb-2">
<.icon name="hero-light-bulb" class="h-3.5 w-3.5 inline" />
Recommended actions
{t("Recommended actions")}
</p>
<div class="flex flex-wrap gap-2">
<.link
@ -358,7 +358,7 @@
<div class="mt-3 rounded-md border border-blue-200 bg-blue-50 p-3 dark:border-blue-800 dark:bg-blue-900/20">
<p class="text-xs font-medium text-blue-800 dark:text-blue-300 mb-2">
<.icon name="hero-light-bulb" class="h-3.5 w-3.5 inline" />
Recommended actions
{t("Recommended actions")}
</p>
<div class="flex flex-wrap gap-2">
<.link
@ -414,7 +414,7 @@
phx-value-id={insight.id}
class="flex-shrink-0 rounded-md px-2.5 py-1.5 text-xs font-medium text-gray-600 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-400 dark:hover:bg-white/10 dark:hover:text-white"
>
Dismiss
{t("Dismiss")}
</button>
<% end %>
</div>

View file

@ -12,7 +12,7 @@ defmodule ToweropsWeb.MapLive.Index do
{:ok,
socket
|> assign(:page_title, "Sites Map")
|> assign(:page_title, t("Sites Map"))
|> assign(:organization, organization)
|> load_sites()}
end
@ -33,7 +33,7 @@ defmodule ToweropsWeb.MapLive.Index do
{:noreply,
socket
|> load_sites()
|> put_flash(:info, "Sites map refreshed")}
|> put_flash(:info, t("Sites map refreshed"))}
end
defp load_sites(socket) do

View file

@ -5,12 +5,12 @@
>
<.header>
<span class="flex items-center gap-2">
Sites Map
{t("Sites Map")}
<span class="inline-flex items-center rounded-full bg-blue-50 px-2 py-0.5 text-xs font-medium text-blue-700 ring-1 ring-inset ring-blue-700/10 dark:bg-blue-400/10 dark:text-blue-400 dark:ring-blue-400/30">
Geographic
{t("Geographic")}
</span>
</span>
<:subtitle>Geographic locations of your sites</:subtitle>
<:subtitle>{t("Geographic locations of your sites")}</:subtitle>
<:actions>
<.button phx-click="refresh_map">
<.icon name="hero-arrow-path" class="h-5 w-5" /> Refresh
@ -27,10 +27,10 @@
<div class="flex items-center justify-between">
<div>
<h3 class="text-sm font-medium text-gray-900 dark:text-white">
Geographic Site Locations
{t("Geographic Site Locations")}
</h3>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
Click markers to view site details
{t("Click markers to view site details")}
</p>
</div>
<div class="flex items-center space-x-2">
@ -64,7 +64,7 @@
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<!-- Site Statistics -->
<div class="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-white/10 p-6">
<h3 class="text-lg font-medium text-gray-900 dark:text-white mb-4">Statistics</h3>
<h3 class="text-lg font-medium text-gray-900 dark:text-white mb-4">{t("Statistics")}</h3>
<div class="space-y-4">
<div>
<div class="text-sm font-medium text-gray-500 dark:text-gray-400">Total Sites</div>
@ -95,7 +95,7 @@
<div class="lg:col-span-2 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-white/10">
<div class="px-6 py-4 border-b border-gray-200 dark:border-white/10">
<h3 class="text-lg font-medium text-gray-900 dark:text-white">
Sites with Geographic Data
{t("Sites with Geographic Data")}
</h3>
</div>
<div class="max-h-96 overflow-y-auto">
@ -145,14 +145,14 @@
<div class="px-6 py-8 text-center">
<.icon name="hero-map-pin" class="mx-auto h-12 w-12 text-gray-400" />
<h3 class="mt-2 text-sm font-medium text-gray-900 dark:text-white">
No geocoded sites
{t("No geocoded sites")}
</h3>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
Add addresses and coordinates to your sites to see them on the map.
{t("Add addresses and coordinates to your sites to see them on the map.")}
</p>
<div class="mt-4">
<.button navigate={~p"/sites"}>
Manage Sites
{t("Manage Sites")}
</.button>
</div>
</div>

View file

@ -25,7 +25,7 @@ defmodule ToweropsWeb.MikrotikBackupLive.Compare do
else
{:noreply,
socket
|> put_flash(:error, "You don't have access to this device")
|> put_flash(:error, t("You don't have access to this device"))
|> push_navigate(to: ~p"/devices")}
end
end
@ -34,7 +34,7 @@ defmodule ToweropsWeb.MikrotikBackupLive.Compare do
def handle_params(_params, _uri, socket) do
{:noreply,
socket
|> put_flash(:error, "Missing required parameters")
|> put_flash(:error, t("Missing required parameters"))
|> push_navigate(to: ~p"/devices")}
end
@ -47,7 +47,7 @@ defmodule ToweropsWeb.MikrotikBackupLive.Compare do
if backup_a.device_id != device_id || backup_b.device_id != device_id do
{:noreply,
socket
|> put_flash(:error, "Backups do not belong to this device")
|> put_flash(:error, t("Backups do not belong to this device"))
|> push_navigate(to: ~p"/devices/#{device_id}?tab=backups")}
else
generate_and_display_diff(socket, device, backup_a, backup_b, device_id)
@ -80,7 +80,7 @@ defmodule ToweropsWeb.MikrotikBackupLive.Compare do
{:error, reason} ->
{:noreply,
socket
|> put_flash(:error, "Failed to generate diff: #{reason}")
|> put_flash(:error, t("Failed to generate diff: %{reason}", reason: reason))
|> push_navigate(to: ~p"/devices/#{device_id}?tab=backups")}
end
end

View file

@ -4,7 +4,7 @@
<div class="mb-6">
<nav class="flex items-center gap-2 text-sm text-gray-500 dark:text-gray-400 mb-4">
<.link navigate={~p"/devices"} class="hover:text-gray-700 dark:hover:text-gray-300">
Devices
{t("Devices")}
</.link>
<span>/</span>
<.link
@ -18,7 +18,7 @@
navigate={~p"/devices/#{@device.id}?tab=backups"}
class="hover:text-gray-700 dark:hover:text-gray-300"
>
Backups
{t("Backups")}
</.link>
<span>/</span>
<span class="text-gray-900 dark:text-white">Compare</span>
@ -26,7 +26,7 @@
<div class="flex items-center justify-between">
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">
Compare Configuration Backups
{t("Compare Configuration Backups")}
</h1>
<.link
navigate={~p"/devices/#{@device.id}?tab=backups"}
@ -135,7 +135,7 @@
<div class="flex items-center gap-2">
<.icon name="hero-check-circle" class="h-5 w-5 text-green-600 dark:text-green-400" />
<p class="text-sm text-green-800 dark:text-green-200">
These backups are identical. No differences found.
{t("These backups are identical. No differences found.")}
</p>
</div>
</div>
@ -166,7 +166,7 @@
<div class="bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10 overflow-hidden">
<div class="px-4 py-3 border-b border-gray-200 dark:border-white/10 bg-gray-50 dark:bg-gray-800/75">
<h3 class="text-sm font-semibold text-gray-900 dark:text-white">
Configuration Differences
{t("Configuration Differences")}
</h3>
</div>
<div class="overflow-x-auto">

View file

@ -46,7 +46,7 @@ defmodule ToweropsWeb.MobileQRLive do
socket =
socket
|> assign(:qr_token, qr_token)
|> put_flash(:info, "QR code expired, generated a new one")
|> put_flash(:info, t("QR code expired, generated a new one"))
schedule_check()
{:noreply, socket}
@ -62,7 +62,7 @@ defmodule ToweropsWeb.MobileQRLive do
socket
|> assign(:completed, true)
|> assign(:mobile_session, mobile_session)
|> put_flash(:info, "Mobile device authenticated successfully!")
|> put_flash(:info, t("Mobile device authenticated successfully!"))
{:noreply, socket}
end

View file

@ -29,7 +29,7 @@ defmodule ToweropsWeb.NetworkMapLive do
{:ok,
socket
|> assign(:page_title, "Network Map")
|> assign(:page_title, t("Network Map"))
|> assign(:timezone, socket.assigns.current_scope.timezone)
|> assign(:loading, true)
|> assign(:topology, default_topology)
@ -55,7 +55,7 @@ defmodule ToweropsWeb.NetworkMapLive do
socket
|> assign(:loading, true)
|> load_topology_data(organization.id, socket.assigns.active_tab)
|> put_flash(:info, "Network map refreshed")}
|> put_flash(:info, t("Network map refreshed"))}
end
@impl true

View file

@ -5,12 +5,12 @@
>
<.header>
<span class="flex items-center gap-2">
Network Map
{t("Network Map")}
<span class="inline-flex items-center rounded-full bg-blue-50 px-2 py-0.5 text-xs font-medium text-blue-700 ring-1 ring-inset ring-blue-700/10 dark:bg-blue-400/10 dark:text-blue-400 dark:ring-blue-400/30">
Experimental
{t("Experimental")}
</span>
</span>
<:subtitle>Visual topology of your network infrastructure</:subtitle>
<:subtitle>{t("Visual topology of your network infrastructure")}</:subtitle>
<:actions>
<.button phx-click="refresh_topology" disabled={@loading}>
<.icon name="hero-arrow-path" class="h-5 w-5" /> Refresh
@ -32,7 +32,7 @@
end
]}
>
Added Devices
{t("Added Devices")}
</.link>
<.link
@ -46,7 +46,7 @@
end
]}
>
All Devices
{t("All Devices")}
<span class="ml-2 inline-flex items-center rounded-full bg-gray-100 px-2 py-0.5 text-xs font-medium text-gray-700 dark:bg-gray-700 dark:text-gray-300">
{@topology.stats.discovered_devices}
</span>
@ -73,7 +73,7 @@
<div class="ml-5 w-0 flex-1">
<dl>
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400 truncate">
Total Devices
{t("Total Devices")}
</dt>
<dd class="text-lg font-medium text-gray-900 dark:text-white">
{@topology.stats.total_devices}
@ -93,7 +93,7 @@
<div class="ml-5 w-0 flex-1">
<dl>
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400 truncate">
Added Devices
{t("Added Devices")}
</dt>
<dd class="text-lg font-medium text-gray-900 dark:text-white">
{@topology.stats.added_devices}
@ -113,7 +113,7 @@
<div class="ml-5 w-0 flex-1">
<dl>
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400 truncate">
Discovered
{t("Discovered")}
</dt>
<dd class="text-lg font-medium text-gray-900 dark:text-white">
{@topology.stats.discovered_devices}
@ -133,7 +133,7 @@
<div class="ml-5 w-0 flex-1">
<dl>
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400 truncate">
Connections
{t("Connections")}
</dt>
<dd class="text-lg font-medium text-gray-900 dark:text-white">
{@topology.stats.total_links}
@ -151,10 +151,10 @@
<div class="flex items-center justify-between">
<div>
<h3 class="text-sm font-medium text-gray-900 dark:text-white">
Topology Visualization
{t("Topology Visualization")}
</h3>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
Click and drag to pan, scroll to zoom, click nodes for details
{t("Click and drag to pan, scroll to zoom, click nodes for details")}
</p>
</div>
<div class="flex items-center space-x-2">
@ -242,13 +242,13 @@
<div class="text-center py-16">
<.icon name="hero-map" 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">
No network topology available
{t("No network topology available")}
</h3>
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">
Add devices with SNMP enabled to start building your network map.
{t("Add devices with SNMP enabled to start building your network map.")}
</p>
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400">
The topology is automatically discovered via LLDP and CDP protocols.
{t("The topology is automatically discovered via LLDP and CDP protocols.")}
</p>
<div class="mt-6">
<.button navigate={~p"/devices/new"} variant="primary">

View file

@ -87,7 +87,7 @@ defmodule ToweropsWeb.Org.GaiiaMappingLive do
case Gaiia.get_network_site(org.id, gaiia_id) do
nil ->
{:noreply, put_flash(socket, :error, "Gaiia network site not found")}
{:noreply, put_flash(socket, :error, t("Gaiia network site not found"))}
network_site ->
case Gaiia.update_network_site_mapping(network_site, %{site_id: site_id}) do
@ -98,10 +98,10 @@ defmodule ToweropsWeb.Org.GaiiaMappingLive do
|> assign(:search_query, "")
|> assign(:search_results, [])
|> load_data()
|> put_flash(:info, "Site linked successfully")}
|> put_flash(:info, t("Site linked successfully"))}
{:error, _} ->
{:noreply, put_flash(socket, :error, "Failed to link site")}
{:noreply, put_flash(socket, :error, t("Failed to link site"))}
end
end
end
@ -112,7 +112,7 @@ defmodule ToweropsWeb.Org.GaiiaMappingLive do
case Gaiia.get_network_site(org.id, gaiia_id) do
nil ->
{:noreply, put_flash(socket, :error, "Gaiia network site not found")}
{:noreply, put_flash(socket, :error, t("Gaiia network site not found"))}
network_site ->
case Gaiia.update_network_site_mapping(network_site, %{site_id: nil}) do
@ -120,10 +120,10 @@ defmodule ToweropsWeb.Org.GaiiaMappingLive do
{:noreply,
socket
|> load_data()
|> put_flash(:info, "Site unlinked")}
|> put_flash(:info, t("Site unlinked"))}
{:error, _} ->
{:noreply, put_flash(socket, :error, "Failed to unlink site")}
{:noreply, put_flash(socket, :error, t("Failed to unlink site"))}
end
end
end
@ -134,7 +134,7 @@ defmodule ToweropsWeb.Org.GaiiaMappingLive do
case Gaiia.get_inventory_item(org.id, gaiia_id) do
nil ->
{:noreply, put_flash(socket, :error, "Gaiia inventory item not found")}
{:noreply, put_flash(socket, :error, t("Gaiia inventory item not found"))}
item ->
case Gaiia.update_inventory_item_mapping(item, %{device_id: device_id}) do
@ -145,10 +145,10 @@ defmodule ToweropsWeb.Org.GaiiaMappingLive do
|> assign(:search_query, "")
|> assign(:search_results, [])
|> load_data()
|> put_flash(:info, "Device linked successfully")}
|> put_flash(:info, t("Device linked successfully"))}
{:error, _} ->
{:noreply, put_flash(socket, :error, "Failed to link device")}
{:noreply, put_flash(socket, :error, t("Failed to link device"))}
end
end
end
@ -159,7 +159,7 @@ defmodule ToweropsWeb.Org.GaiiaMappingLive do
case Gaiia.get_inventory_item(org.id, gaiia_id) do
nil ->
{:noreply, put_flash(socket, :error, "Gaiia inventory item not found")}
{:noreply, put_flash(socket, :error, t("Gaiia inventory item not found"))}
item ->
case Gaiia.update_inventory_item_mapping(item, %{device_id: nil}) do
@ -167,10 +167,10 @@ defmodule ToweropsWeb.Org.GaiiaMappingLive do
{:noreply,
socket
|> load_data()
|> put_flash(:info, "Device unlinked")}
|> put_flash(:info, t("Device unlinked"))}
{:error, _} ->
{:noreply, put_flash(socket, :error, "Failed to unlink device")}
{:noreply, put_flash(socket, :error, t("Failed to unlink device"))}
end
end
end

View file

@ -12,10 +12,10 @@
</.link>
</div>
<h1 class="text-3xl font-semibold tracking-tight text-gray-900 dark:text-white">
Gaiia Entity Mapping
{t("Gaiia Entity Mapping")}
</h1>
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
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>
@ -26,13 +26,13 @@
navigate={~p"/orgs/#{@organization.slug}/settings/integrations/gaiia/mapping"}
class="border-b-2 border-indigo-500 px-1 pb-3 text-sm font-medium text-indigo-600 dark:text-indigo-400"
>
Entity Mapping
{t("Entity Mapping")}
</.link>
<.link
navigate={~p"/orgs/#{@organization.slug}/settings/integrations/gaiia/reconciliation"}
class="border-b-2 border-transparent px-1 pb-3 text-sm font-medium text-gray-500 hover:border-gray-300 hover:text-gray-700 dark:text-gray-400 dark:hover:border-gray-600 dark:hover:text-white"
>
Reconciliation
{t("Reconciliation")}
</.link>
</nav>
</div>
@ -54,7 +54,7 @@
)
]}
>
Network Sites
{t("Network Sites")}
</.link>
<.link
id="tab-devices"
@ -70,7 +70,7 @@
)
]}
>
Inventory Items
{t("Inventory Items")}
</.link>
</nav>
</div>
@ -108,19 +108,19 @@
/>
<h3 class="mt-2 text-sm font-semibold text-gray-900 dark:text-white">
<%= if @tab == "sites" do %>
No network sites found
{t("No network sites found")}
<% else %>
No inventory items found
{t("No inventory items found")}
<% end %>
</h3>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
<%= case @filter do %>
<% "mapped" -> %>
No mapped entities yet. Sync from Gaiia and link entities below.
{t("No mapped entities yet. Sync from Gaiia and link entities below.")}
<% "unmapped" -> %>
All entities are mapped.
{t("All entities are mapped.")}
<% _ -> %>
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>
@ -133,20 +133,20 @@
scope="col"
class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400"
>
Name
{t("Name")}
</th>
<th
scope="col"
class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400"
>
Gaiia ID
{t("Gaiia ID")}
</th>
<%= if @tab == "devices" do %>
<th
scope="col"
class="hidden px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400 sm:table-cell"
>
IP Address
{t("IP Address")}
</th>
<% end %>
<th
@ -159,7 +159,7 @@
scope="col"
class="px-4 py-3 text-right text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400"
>
Actions
{t("Actions")}
</th>
</tr>
</thead>
@ -210,10 +210,10 @@
id={"unlink-#{item.id}"}
phx-click="unlink_site"
phx-value-gaiia-id={item.gaiia_id}
data-confirm="Unlink this site?"
data-confirm={t("Unlink this site?")}
class="text-red-600 hover:text-red-800 dark:text-red-400 dark:hover:text-red-300"
>
Unlink
{t("Unlink")}
</button>
<% else %>
<button
@ -223,7 +223,7 @@
phx-value-id={item.gaiia_id}
class="text-indigo-600 hover:text-indigo-800 dark:text-indigo-400 dark:hover:text-indigo-300"
>
Link
{t("Link")}
</button>
<% end %>
<% else %>
@ -233,10 +233,10 @@
id={"unlink-#{item.id}"}
phx-click="unlink_device"
phx-value-gaiia-id={item.gaiia_id}
data-confirm="Unlink this device?"
data-confirm={t("Unlink this device?")}
class="text-red-600 hover:text-red-800 dark:text-red-400 dark:hover:text-red-300"
>
Unlink
{t("Unlink")}
</button>
<% else %>
<button
@ -246,7 +246,7 @@
phx-value-id={item.gaiia_id}
class="text-indigo-600 hover:text-indigo-800 dark:text-indigo-400 dark:hover:text-indigo-300"
>
Link
{t("Link")}
</button>
<% end %>
<% end %>
@ -269,7 +269,7 @@
<%= if suggestions = @suggestions[item.gaiia_id] do %>
<div class="mb-3">
<p class="mb-1 text-xs font-medium text-amber-700 dark:text-amber-400">
Suggested matches:
{t("Suggested matches:")}
</p>
<div class="flex flex-wrap gap-2">
<%= for suggestion <- suggestions do %>
@ -360,7 +360,7 @@
phx-click="cancel_link"
class="self-start text-sm text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300"
>
Cancel
{t("Cancel")}
</button>
</div>
</td>

View file

@ -11,7 +11,7 @@ defmodule ToweropsWeb.Org.GaiiaReconciliationLive do
{:ok,
socket
|> assign(:organization, org)
|> assign(:page_title, "Gaiia Inventory Reconciliation")}
|> assign(:page_title, t("Gaiia Inventory Reconciliation"))}
end
@impl true

View file

@ -4,9 +4,9 @@
active_page="settings"
>
<.header>
Gaiia Inventory Reconciliation
{t("Gaiia Inventory Reconciliation")}
<:subtitle>
Compare Gaiia inventory against Towerops device discovery. Reconciliation runs nightly.
{t("Compare Gaiia inventory against Towerops device discovery. Reconciliation runs nightly.")}
</:subtitle>
</.header>
@ -17,13 +17,13 @@
navigate={~p"/orgs/#{@organization.slug}/settings/integrations/gaiia/mapping"}
class="border-b-2 border-transparent px-1 pb-3 text-sm font-medium text-gray-500 hover:border-gray-300 hover:text-gray-700 dark:text-gray-400 dark:hover:border-gray-600 dark:hover:text-white"
>
Entity Mapping
{t("Entity Mapping")}
</.link>
<.link
navigate={~p"/orgs/#{@organization.slug}/settings/integrations/gaiia/reconciliation"}
class="border-b-2 border-indigo-500 px-1 pb-3 text-sm font-medium text-indigo-600 dark:text-indigo-400"
>
Reconciliation
{t("Reconciliation")}
</.link>
</nav>
</div>
@ -69,7 +69,7 @@
"bg-white text-gray-700 hover:bg-gray-50 dark:bg-gray-800/50 dark:text-gray-300 dark:hover:bg-gray-800"
]}
>
Summary
{t("Summary")}
</.link>
<.link
patch={
@ -121,7 +121,7 @@
<div class="space-y-2">
<%= if @report.untracked_devices == [] do %>
<p class="py-8 text-center text-sm text-gray-500 dark:text-gray-400">
All Towerops devices have matching Gaiia inventory items.
{t("All Towerops devices have matching Gaiia inventory items.")}
</p>
<% else %>
<div class="overflow-hidden rounded-lg border border-gray-200 dark:border-white/10">
@ -129,13 +129,13 @@
<thead class="bg-gray-50 dark:bg-gray-800/50">
<tr>
<th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">
Device
{t("Device")}
</th>
<th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">
IP Address
{t("IP Address")}
</th>
<th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">
Site
{t("Site")}
</th>
</tr>
</thead>
@ -169,7 +169,7 @@
<div class="space-y-2">
<%= if @report.data_mismatches == [] do %>
<p class="py-8 text-center text-sm text-gray-500 dark:text-gray-400">
No data mismatches found between mapped devices.
{t("No data mismatches found between mapped devices.")}
</p>
<% else %>
<div class="overflow-hidden rounded-lg border border-gray-200 dark:border-white/10">
@ -177,16 +177,16 @@
<thead class="bg-gray-50 dark:bg-gray-800/50">
<tr>
<th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">
Device
{t("Device")}
</th>
<th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">
Field
{t("Field")}
</th>
<th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">
Towerops
{t("Towerops")}
</th>
<th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">
Gaiia
{t("Gaiia")}
</th>
</tr>
</thead>
@ -221,7 +221,7 @@
<div class="space-y-2">
<%= if @report.missing_mappings == [] do %>
<p class="py-8 text-center text-sm text-gray-500 dark:text-gray-400">
All Gaiia inventory items are mapped to Towerops devices.
{t("All Gaiia inventory items are mapped to Towerops devices.")}
</p>
<% else %>
<div class="overflow-hidden rounded-lg border border-gray-200 dark:border-white/10">
@ -229,16 +229,16 @@
<thead class="bg-gray-50 dark:bg-gray-800/50">
<tr>
<th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">
Gaiia Item
{t("Gaiia Item")}
</th>
<th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">
IP Address
{t("IP Address")}
</th>
<th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">
Category
{t("Category")}
</th>
<th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">
Status
{t("Status")}
</th>
</tr>
</thead>
@ -281,10 +281,10 @@
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">
No inventory data
{t("No inventory data")}
</h3>
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">
Sync Gaiia data and map inventory items to see reconciliation results.
{t("Sync Gaiia data and map inventory items to see reconciliation results.")}
</p>
<.link
navigate={~p"/orgs/#{@organization.slug}/settings/integrations/gaiia/mapping"}
@ -295,7 +295,7 @@
</div>
<% else %>
<div class="rounded-lg border border-gray-200 bg-white p-6 dark:border-white/10 dark:bg-gray-800/50">
<h3 class="text-sm font-semibold text-gray-900 dark:text-white">Overview</h3>
<h3 class="text-sm font-semibold text-gray-900 dark:text-white">{t("Overview")}</h3>
<dl class="mt-4 space-y-3">
<div class="flex justify-between text-sm">
<dt class="text-gray-600 dark:text-gray-400">
@ -307,7 +307,7 @@
</div>
<div class="flex justify-between text-sm">
<dt class="text-gray-600 dark:text-gray-400">
Unmapped Gaiia items (no Towerops link)
{t("Unmapped Gaiia items (no Towerops link)")}
</dt>
<dd class="font-medium text-yellow-600 dark:text-yellow-400">
{@report.summary.missing_mapping_count}
@ -315,7 +315,7 @@
</div>
<div class="flex justify-between text-sm">
<dt class="text-gray-600 dark:text-gray-400">
Untracked Towerops devices (not in Gaiia)
{t("Untracked Towerops devices (not in Gaiia)")}
</dt>
<dd class="font-medium text-blue-600 dark:text-blue-400">
{@report.summary.untracked_count}
@ -323,7 +323,7 @@
</div>
<div class="flex justify-between text-sm">
<dt class="text-gray-600 dark:text-gray-400">
Data mismatches (IP, serial, etc.)
{t("Data mismatches (IP, serial, etc.)")}
</dt>
<dd class="font-medium text-red-600 dark:text-red-400">
{@report.summary.mismatch_count}

View file

@ -122,7 +122,7 @@ defmodule ToweropsWeb.Org.IntegrationsLive do
|> assign(:configuring, nil)
|> assign(:form, nil)
|> assign(:test_result, nil)
|> put_flash(:info, "Integration saved successfully")}
|> put_flash(:info, t("Integration saved successfully"))}
{:error, changeset} ->
{:noreply, assign(socket, :form, to_form(changeset))}
@ -135,7 +135,7 @@ defmodule ToweropsWeb.Org.IntegrationsLive do
provider = socket.assigns.configuring
if api_key == "" or is_nil(api_key) do
{:noreply, assign(socket, :test_result, {:error, "Please enter an API key first"})}
{:noreply, assign(socket, :test_result, {:error, t("Please enter an API key first")})}
else
result = test_provider_connection(provider, api_key)
{:noreply, assign(socket, :test_result, format_connection_result(result))}
@ -161,10 +161,10 @@ defmodule ToweropsWeb.Org.IntegrationsLive do
{:noreply,
socket
|> assign(:integrations, integrations)
|> put_flash(:info, "Webhook secret regenerated")}
|> put_flash(:info, t("Webhook secret regenerated"))}
{:error, _changeset} ->
{:noreply, put_flash(socket, :error, "Failed to regenerate webhook secret")}
{:noreply, put_flash(socket, :error, t("Failed to regenerate webhook secret"))}
end
end
end
@ -183,10 +183,10 @@ defmodule ToweropsWeb.Org.IntegrationsLive do
{:noreply,
socket
|> assign(:integrations, integrations)
|> put_flash(:info, "Integration updated")}
|> put_flash(:info, t("Integration updated"))}
{:error, _changeset} ->
{:noreply, put_flash(socket, :error, "Failed to update integration")}
{:noreply, put_flash(socket, :error, t("Failed to update integration"))}
end
end
end

View file

@ -12,10 +12,10 @@
</.link>
</div>
<h1 class="text-3xl font-semibold tracking-tight text-gray-900 dark:text-white">
Integrations
{t("Integrations")}
</h1>
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
Connect third-party services to enhance your monitoring capabilities.
{t("Connect third-party services to enhance your monitoring capabilities.")}
</p>
</div>
@ -51,7 +51,7 @@
<%= if integration.last_synced_at do %>
<span class="text-xs text-gray-500 dark:text-gray-400">
Last synced:
{t("Last synced:")}
<.timestamp
datetime={integration.last_synced_at}
timezone={@timezone}
@ -172,7 +172,7 @@
phx-value-provider={provider.id}
class="rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-xs ring-1 ring-inset ring-gray-300 hover:bg-gray-50 dark:bg-white/10 dark:text-white dark:ring-white/10 dark:hover:bg-white/20"
>
Configure
{t("Configure")}
</button>
</div>
</div>
@ -189,7 +189,7 @@
<.input
field={@form[:api_key]}
type="password"
label="API Key"
label={t("API Key")}
placeholder={"Enter your #{provider.name} API key"}
autocomplete="off"
value={get_credential(@integrations[provider.id], "api_key")}
@ -198,7 +198,7 @@
<.input
field={@form[:sync_interval_minutes]}
type="number"
label="Sync interval (minutes)"
label={t("Sync interval (minutes)")}
min="5"
value={
if(@integrations[provider.id],
@ -235,7 +235,7 @@
phx-click="test_connection"
class="rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-xs ring-1 ring-inset ring-gray-300 hover:bg-gray-50 dark:bg-white/10 dark:text-white dark:ring-white/10 dark:hover:bg-white/20"
>
Test Connection
{t("Test Connection")}
</button>
<div class="flex items-center gap-3">
@ -244,14 +244,14 @@
phx-click="close_config"
class="text-sm font-semibold text-gray-900 dark:text-white"
>
Cancel
{t("Cancel")}
</button>
<button
type="submit"
phx-disable-with="Saving..."
phx-disable-with={t("Saving...")}
class="rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-xs hover:bg-indigo-500 dark:bg-indigo-500 dark:hover:bg-indigo-400"
>
Save
{t("Save")}
</button>
</div>
</div>
@ -263,16 +263,16 @@
<%= if provider.id == "gaiia" && @integrations["gaiia"] do %>
<div class="mt-6 border-t border-gray-200 pt-6 dark:border-white/10">
<h4 class="text-sm font-semibold text-gray-900 dark:text-white">
Webhook Configuration
{t("Webhook Configuration")}
</h4>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
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">
<div>
<label class="block text-xs font-medium text-gray-700 dark:text-gray-300">
Webhook URL
{t("Webhook URL")}
</label>
<div class="mt-1 flex items-center gap-2">
<input
@ -296,7 +296,7 @@
<div>
<label class="block text-xs font-medium text-gray-700 dark:text-gray-300">
Webhook Secret
{t("Webhook Secret")}
</label>
<div class="mt-1 flex items-center gap-2">
<input
@ -321,17 +321,17 @@
type="button"
id="regenerate-webhook-secret"
phx-click="regenerate_webhook_secret"
data-confirm="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"
>
Regenerate Secret
{t("Regenerate Secret")}
</button>
</div>
</div>
<div class="rounded-md bg-blue-50 p-3 dark:bg-blue-900/20">
<h5 class="text-xs font-medium text-blue-800 dark:text-blue-300">
Setup Instructions
{t("Setup Instructions")}
</h5>
<ol class="mt-2 list-inside list-decimal space-y-1 text-xs text-blue-700 dark:text-blue-400">
<li>In your Gaiia admin panel, go to Settings → Webhooks</li>
@ -342,7 +342,7 @@
<li>Save the webhook</li>
</ol>
<p class="mt-2 text-xs text-blue-600 dark:text-blue-400">
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

@ -83,10 +83,10 @@ defmodule ToweropsWeb.Org.PreseemDevicesLive do
|> assign(:device_search, "")
|> assign(:search_results, [])
|> load_access_points()
|> put_flash(:info, "Device linked successfully")}
|> put_flash(:info, t("Device linked successfully"))}
{:error, _} ->
{:noreply, put_flash(socket, :error, "Failed to link device")}
{:noreply, put_flash(socket, :error, t("Failed to link device"))}
end
end
@ -97,10 +97,10 @@ defmodule ToweropsWeb.Org.PreseemDevicesLive do
{:noreply,
socket
|> load_access_points()
|> put_flash(:info, "Device unlinked")}
|> put_flash(:info, t("Device unlinked"))}
{:error, _} ->
{:noreply, put_flash(socket, :error, "Failed to unlink device")}
{:noreply, put_flash(socket, :error, t("Failed to unlink device"))}
end
end

View file

@ -12,10 +12,10 @@
</.link>
</div>
<h1 class="text-3xl font-semibold tracking-tight text-gray-900 dark:text-white">
Preseem Devices
{t("Preseem Devices")}
</h1>
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
Manage device matching between Preseem access points and your monitored devices.
{t("Manage device matching between Preseem access points and your monitored devices.")}
</p>
</div>
@ -26,13 +26,13 @@
navigate={~p"/orgs/#{@organization.slug}/settings/integrations/preseem/devices"}
class="border-b-2 border-indigo-500 px-1 pb-3 text-sm font-medium text-indigo-600 dark:text-indigo-400"
>
Devices
{t("Devices")}
</.link>
<.link
navigate={~p"/insights?source=preseem"}
class="border-b-2 border-transparent px-1 pb-3 text-sm font-medium text-gray-500 hover:border-gray-300 hover:text-gray-700 dark:text-gray-400 dark:hover:border-gray-600 dark:hover:text-white"
>
Insights
{t("Insights")}
</.link>
</nav>
</div>
@ -52,7 +52,7 @@
)
]}
>
All
{t("All")}
</.link>
<.link
id="filter-unmatched"
@ -68,7 +68,7 @@
)
]}
>
Unmatched
{t("Unmatched")}
</.link>
<.link
id="filter-matched"
@ -84,7 +84,7 @@
)
]}
>
Matched
{t("Matched")}
</.link>
</nav>
</div>
@ -98,16 +98,16 @@
>
<.icon name="hero-signal" class="mx-auto h-12 w-12 text-gray-400 dark:text-gray-500" />
<h3 class="mt-2 text-sm font-semibold text-gray-900 dark:text-white">
No access points found
{t("No access points found")}
</h3>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
<%= case @filter do %>
<% "unmatched" -> %>
All access points are matched. Nice!
{t("All access points are matched. Nice!")}
<% "matched" -> %>
No matched access points yet. Sync from Preseem and link devices.
{t("No matched access points yet. Sync from Preseem and link devices.")}
<% _ -> %>
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>
@ -120,37 +120,37 @@
scope="col"
class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400"
>
Name
{t("Name")}
</th>
<th
scope="col"
class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400"
>
Preseem ID
{t("Preseem ID")}
</th>
<th
scope="col"
class="hidden px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400 sm:table-cell"
>
IP Address
{t("IP Address")}
</th>
<th
scope="col"
class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400"
>
Status
{t("Status")}
</th>
<th
scope="col"
class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400"
>
Linked Device
{t("Linked Device")}
</th>
<th
scope="col"
class="px-4 py-3 text-right text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400"
>
Actions
{t("Actions")}
</th>
</tr>
</thead>
@ -196,10 +196,10 @@
id={"unlink-ap-#{ap.id}"}
phx-click="unlink_device"
phx-value-ap-id={ap.id}
data-confirm="Unlink this device from the Preseem access point?"
data-confirm={t("Unlink this device from the Preseem access point?")}
class="text-red-600 hover:text-red-800 dark:text-red-400 dark:hover:text-red-300"
>
Unlink
{t("Unlink")}
</button>
<% else %>
<button
@ -209,7 +209,7 @@
phx-value-ap-id={ap.id}
class="text-indigo-600 hover:text-indigo-800 dark:text-indigo-400 dark:hover:text-indigo-300"
>
Link
{t("Link")}
</button>
<% end %>
</td>
@ -229,7 +229,7 @@
id="device-search-input"
name="query"
value={@device_search}
placeholder="Search by device name or IP..."
placeholder={t("Search by device name or IP...")}
autocomplete="off"
phx-debounce="300"
class="block w-full rounded-md border-gray-300 text-sm shadow-sm focus:border-indigo-500 focus:ring-indigo-500 dark:border-white/10 dark:bg-white/5 dark:text-white"
@ -277,7 +277,7 @@
phx-click="cancel_link"
class="self-start text-sm text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300"
>
Cancel
{t("Cancel")}
</button>
</div>
</td>

View file

@ -64,10 +64,10 @@ defmodule ToweropsWeb.Org.PreseemInsightsLive do
{:noreply,
socket
|> load_insights()
|> put_flash(:info, "Insight dismissed")}
|> put_flash(:info, t("Insight dismissed"))}
{:error, _} ->
{:noreply, put_flash(socket, :error, "Failed to dismiss")}
{:noreply, put_flash(socket, :error, t("Failed to dismiss"))}
end
end
@ -110,7 +110,7 @@ defmodule ToweropsWeb.Org.PreseemInsightsLive do
|> put_flash(:info, "#{count} insight(s) dismissed")}
{:error, _} ->
{:noreply, put_flash(socket, :error, "Failed to dismiss insights")}
{:noreply, put_flash(socket, :error, t("Failed to dismiss insights"))}
end
end
end

View file

@ -14,10 +14,10 @@
<div class="flex items-center justify-between">
<div>
<h1 class="text-3xl font-semibold tracking-tight text-gray-900 dark:text-white">
Network Insights
{t("Network Insights")}
</h1>
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
Proactive network health observations generated from Preseem data analysis.
{t("Proactive network health observations generated from Preseem data analysis.")}
</p>
</div>
</div>
@ -41,7 +41,7 @@
)
]}
>
Active
{t("Active")}
</.link>
<.link
id="filter-dismissed"
@ -57,7 +57,7 @@
)
]}
>
Dismissed
{t("Dismissed")}
</.link>
</nav>
@ -77,7 +77,7 @@
)
]}
>
All
{t("All")}
</.link>
<.link
id="filter-urgency-critical"
@ -92,7 +92,7 @@
)
]}
>
Critical
{t("Critical")}
</.link>
<.link
id="filter-urgency-warning"
@ -107,7 +107,7 @@
)
]}
>
Warning
{t("Warning")}
</.link>
<.link
id="filter-urgency-info"
@ -122,7 +122,7 @@
)
]}
>
Info
{t("Info")}
</.link>
</div>
</div>
@ -142,7 +142,7 @@
phx-click="deselect_all"
class="text-sm text-indigo-600 hover:text-indigo-800 dark:text-indigo-400 dark:hover:text-indigo-300"
>
Deselect All
{t("Deselect All")}
</button>
<button
type="button"
@ -150,7 +150,7 @@
phx-click="bulk_dismiss"
class="rounded-md bg-indigo-600 px-3 py-1.5 text-sm font-medium text-white shadow-xs hover:bg-indigo-500 dark:bg-indigo-500 dark:hover:bg-indigo-400"
>
Dismiss Selected
{t("Dismiss Selected")}
</button>
</div>
<% end %>
@ -167,16 +167,16 @@
class="mx-auto h-12 w-12 text-gray-400 dark:text-gray-500"
/>
<h3 class="mt-2 text-sm font-semibold text-gray-900 dark:text-white">
No insights found
{t("No insights found")}
</h3>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
<%= cond do %>
<% @filter_status == "dismissed" -> %>
No dismissed insights.
{t("No dismissed insights.")}
<% @filter_urgency != nil -> %>
No {@filter_urgency} insights right now.
<% true -> %>
No active insights. Your network is looking healthy!
{t("No active insights. Your network is looking healthy!")}
<% end %>
</p>
</div>
@ -189,7 +189,7 @@
phx-click="select_all"
class="text-sm text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white"
>
Select All
{t("Select All")}
</button>
</div>
@ -296,7 +296,7 @@
phx-value-id={insight.id}
class="flex-shrink-0 rounded-md px-2.5 py-1.5 text-xs font-medium text-gray-600 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-400 dark:hover:bg-white/10 dark:hover:text-white"
>
Dismiss
{t("Dismiss")}
</button>
<% end %>
</div>

View file

@ -126,7 +126,7 @@ defmodule ToweropsWeb.Org.SettingsLive do
{:noreply,
socket
|> put_flash(:info, "Settings saved successfully")
|> put_flash(:info, t("Settings saved successfully"))
|> assign(:organization, organization)
|> assign(:form, to_form(changeset))}
@ -139,14 +139,14 @@ 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, "Applied SNMP configuration to #{count} device records across all sites")}
{: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, "Applied default agent to #{count} device records across all sites")}
{:noreply, put_flash(socket, :info, t("Applied default agent to %{count} device records across all sites", count: count))}
end
# === Members tab events ===
@ -171,12 +171,12 @@ defmodule ToweropsWeb.Org.SettingsLive do
{:noreply,
socket
|> put_flash(:info, "Invitation sent to #{email}")
|> put_flash(:info, t("Invitation sent to %{email}", email: email))
|> assign(:pending_invitations, Organizations.list_pending_invitations(organization.id))
|> assign(:invite_form, to_form(%{"email" => "", "role" => "member"}))}
{:error, changeset} ->
{:noreply, put_flash(socket, :error, "Failed to send invitation: #{error_messages(changeset)}")}
{:noreply, put_flash(socket, :error, t("Failed to send invitation: %{errors}", errors: error_messages(changeset)))}
end
end
@ -190,10 +190,10 @@ defmodule ToweropsWeb.Org.SettingsLive do
{:noreply,
socket
|> put_flash(:info, "Invitation cancelled")
|> put_flash(:info, t("Invitation cancelled"))
|> assign(:pending_invitations, Organizations.list_pending_invitations(organization.id))}
else
{:noreply, put_flash(socket, :error, "Invitation not found")}
{:noreply, put_flash(socket, :error, t("Invitation not found"))}
end
end
@ -205,14 +205,14 @@ defmodule ToweropsWeb.Org.SettingsLive do
{:ok, _} ->
{:noreply,
socket
|> put_flash(:info, "Member removed")
|> put_flash(:info, t("Member removed"))
|> assign(:members, Organizations.list_organization_members(organization.id))}
{:error, :cannot_remove_owner} ->
{:noreply, put_flash(socket, :error, "Cannot remove the organization owner")}
{:noreply, put_flash(socket, :error, t("Cannot remove the organization owner"))}
{:error, _} ->
{:noreply, put_flash(socket, :error, "Failed to remove member")}
{:noreply, put_flash(socket, :error, t("Failed to remove member"))}
end
end
@ -224,14 +224,14 @@ defmodule ToweropsWeb.Org.SettingsLive do
{:ok, _} ->
{:noreply,
socket
|> put_flash(:info, "Role updated")
|> put_flash(:info, t("Role updated"))
|> assign(:members, Organizations.list_organization_members(organization.id))}
{:error, :cannot_change_owner_role} ->
{:noreply, put_flash(socket, :error, "Cannot change the owner's role")}
{:noreply, put_flash(socket, :error, t("Cannot change the owner's role"))}
{:error, _} ->
{:noreply, put_flash(socket, :error, "Failed to update role")}
{:noreply, put_flash(socket, :error, t("Failed to update role"))}
end
end
@ -242,7 +242,7 @@ defmodule ToweropsWeb.Org.SettingsLive do
membership = socket.assigns.membership
if membership.is_default do
{:noreply, put_flash(socket, :error, "This is already your default organization")}
{:noreply, put_flash(socket, :error, t("This is already your default organization"))}
else
case Organizations.set_default_organization(user.id, organization.id) do
{:ok, _} ->
@ -254,7 +254,7 @@ defmodule ToweropsWeb.Org.SettingsLive do
|> assign(:membership, updated_membership)}
{:error, _} ->
{:noreply, put_flash(socket, :error, "Failed to set default organization")}
{:noreply, put_flash(socket, :error, t("Failed to set default organization"))}
end
end
end
@ -371,7 +371,7 @@ defmodule ToweropsWeb.Org.SettingsLive do
|> assign(:configuring, nil)
|> assign(:integration_form, nil)
|> assign(:test_result, nil)
|> put_flash(:info, "Integration saved successfully")}
|> put_flash(:info, t("Integration saved successfully"))}
{:error, changeset} ->
{:noreply, assign(socket, :integration_form, to_form(changeset))}
@ -388,7 +388,7 @@ defmodule ToweropsWeb.Org.SettingsLive do
cond do
url == "" or is_nil(url) ->
{:noreply, assign(socket, :test_result, {:error, "Please enter your NetBox URL first"})}
{:noreply, assign(socket, :test_result, {:error, t("Please enter your NetBox URL first")})}
token == "" or is_nil(token) ->
{:noreply, assign(socket, :test_result, {:error, "Please enter your NetBox API token first"})}
@ -401,7 +401,7 @@ defmodule ToweropsWeb.Org.SettingsLive do
api_key = extract_api_key(socket)
if api_key == "" or is_nil(api_key) do
{:noreply, assign(socket, :test_result, {:error, "Please enter an API key first"})}
{:noreply, assign(socket, :test_result, {:error, t("Please enter an API key first")})}
else
result = test_provider_connection(provider, api_key)
{:noreply, assign(socket, :test_result, format_connection_result(result))}
@ -426,10 +426,10 @@ defmodule ToweropsWeb.Org.SettingsLive do
{:noreply,
socket
|> assign(:integrations, integrations)
|> put_flash(:info, "Webhook secret saved")}
|> put_flash(:info, t("Webhook secret saved"))}
{:error, _changeset} ->
{:noreply, put_flash(socket, :error, "Failed to save webhook secret")}
{:noreply, put_flash(socket, :error, t("Failed to save webhook secret"))}
end
end
end
@ -448,10 +448,10 @@ defmodule ToweropsWeb.Org.SettingsLive do
{:noreply,
socket
|> assign(:integrations, integrations)
|> put_flash(:info, "Integration updated")}
|> put_flash(:info, t("Integration updated"))}
{:error, _changeset} ->
{:noreply, put_flash(socket, :error, "Failed to update integration")}
{:noreply, put_flash(socket, :error, t("Failed to update integration"))}
end
end
end

View file

@ -12,10 +12,10 @@
</.link>
</div>
<h1 class="text-3xl font-semibold tracking-tight text-gray-900 dark:text-white">
Organization Settings
{t("Organization Settings")}
</h1>
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
Manage organization defaults, integrations, and configuration
{t("Manage organization defaults, integrations, and configuration")}
</p>
</div>
@ -32,7 +32,7 @@
if @active_tab == "general", do: "text-indigo-600 dark:text-indigo-400", else: ""
}
>
General
{t("General")}
</.link>
</li>
<li>
@ -40,7 +40,7 @@
patch={~p"/orgs/#{@organization.slug}/settings?tab=snmp"}
class={if @active_tab == "snmp", do: "text-indigo-600 dark:text-indigo-400", else: ""}
>
SNMP
{t("SNMP")}
</.link>
</li>
<%= if @current_scope.user.is_superuser do %>
@ -51,7 +51,7 @@
if @active_tab == "mikrotik", do: "text-indigo-600 dark:text-indigo-400", else: ""
}
>
MikroTik
{t("MikroTik")}
</.link>
</li>
<% end %>
@ -62,7 +62,7 @@
if @active_tab == "agents", do: "text-indigo-600 dark:text-indigo-400", else: ""
}
>
Agents
{t("Agents")}
</.link>
</li>
<li>
@ -72,7 +72,7 @@
if @active_tab == "members", do: "text-indigo-600 dark:text-indigo-400", else: ""
}
>
Members
{t("Members")}
</.link>
</li>
<li>
@ -84,7 +84,7 @@
else: ""
}
>
Integrations
{t("Integrations")}
</.link>
</li>
</ul>
@ -106,10 +106,10 @@
<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">
<div>
<h2 class="text-base/7 font-semibold text-gray-900 dark:text-white">
Organization Name
{t("Organization Name")}
</h2>
<p class="mt-1 text-sm/6 text-gray-500 dark:text-gray-400">
Update the name of your organization.
{t("Update the name of your organization.")}
</p>
</div>
@ -117,7 +117,7 @@
<.input
field={@form[:name]}
type="text"
label="Organization Name"
label={t("Organization Name")}
required
/>
</div>
@ -128,10 +128,10 @@
<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">
<div>
<h2 class="text-base/7 font-semibold text-gray-900 dark:text-white">
Default Organization
{t("Default Organization")}
</h2>
<p class="mt-1 text-sm/6 text-gray-500 dark:text-gray-400">
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>
@ -144,7 +144,7 @@
class="h-5 w-5 text-green-600 dark:text-green-400"
/>
<p class="ml-3 text-sm text-green-800 dark:text-green-200">
This is your default organization
{t("This is your default organization")}
</p>
</div>
</div>
@ -157,7 +157,7 @@
<.icon name="hero-star" class="h-4 w-4 inline" /> Set as Default Organization
</button>
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
Click to make this your default organization
{t("Click to make this your default organization")}
</p>
<% end %>
</div>
@ -168,10 +168,10 @@
<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">
<div>
<h2 class="text-base/7 font-semibold text-gray-900 dark:text-white">
Site Organization
{t("Site Organization")}
</h2>
<p class="mt-1 text-sm/6 text-gray-500 dark:text-gray-400">
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>
@ -179,10 +179,10 @@
<.input
field={@form[:use_sites]}
type="checkbox"
label="Use sites to organize devices"
label={t("Use sites to organize devices")}
/>
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
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 %>
@ -193,12 +193,12 @@
</div>
<div class="ml-3">
<h3 class="text-sm font-medium text-amber-800 dark:text-amber-200">
Warning: Disabling Sites
{t("Warning: Disabling Sites")}
</h3>
<div class="mt-2 text-sm text-amber-700 dark:text-amber-300">
<p>
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.
{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>
@ -214,10 +214,10 @@
<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">
<div>
<h2 class="text-base/7 font-semibold text-gray-900 dark:text-white">
SNMP Configuration
{t("SNMP Configuration")}
</h2>
<p class="mt-1 text-sm/6 text-gray-500 dark:text-gray-400">
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" />
@ -229,7 +229,7 @@
<.input
field={@form[:snmp_version]}
type="select"
label="SNMP Version"
label={t("SNMP Version")}
prompt="Select SNMP version"
options={[{"v1", "1"}, {"v2c", "2c"}, {"v3", "3"}]}
/>
@ -239,7 +239,7 @@
<.input
field={@form[:snmp_community]}
type="text"
label="SNMP Community String"
label={t("SNMP Community String")}
placeholder="e.g., public"
/>
<% end %>
@ -249,7 +249,7 @@
<.input
field={@form[:snmpv3_security_level]}
type="select"
label="Security Level"
label={t("Security Level")}
prompt="Select security level"
options={[
{"No Auth, No Priv", "noAuthNoPriv"},
@ -261,7 +261,7 @@
<.input
field={@form[:snmpv3_username]}
type="text"
label="Username"
label={t("Username")}
placeholder="e.g., snmpuser"
/>
@ -270,7 +270,7 @@
<.input
field={@form[:snmpv3_auth_protocol]}
type="select"
label="Auth Protocol"
label={t("Auth Protocol")}
prompt="Select protocol"
options={[
{"SHA-256 (recommended)", "SHA-256"},
@ -285,8 +285,8 @@
<.input
field={@form[:snmpv3_auth_password]}
type="password"
label="Auth Password"
placeholder="Min 8 characters"
label={t("Auth Password")}
placeholder={t("Min 8 characters")}
autocomplete="off"
/>
<% end %>
@ -296,7 +296,7 @@
<.input
field={@form[:snmpv3_priv_protocol]}
type="select"
label="Privacy Protocol"
label={t("Privacy Protocol")}
prompt="Select protocol"
options={[
{"AES-128 (recommended)", "AES"},
@ -310,8 +310,8 @@
<.input
field={@form[:snmpv3_priv_password]}
type="password"
label="Privacy Password"
placeholder="Min 8 characters"
label={t("Privacy Password")}
placeholder={t("Min 8 characters")}
autocomplete="off"
/>
<% end %>
@ -320,26 +320,26 @@
<.input
field={@form[:snmp_port]}
type="number"
label="SNMP Port"
label={t("SNMP Port")}
placeholder="161"
/>
<%= if @organization.snmp_community do %>
<div class="mt-6 rounded-md bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 p-4">
<h3 class="text-sm font-medium text-amber-900 dark:text-amber-200 mb-2">
Force Apply to All Devices
{t("Force Apply to All Devices")}
</h3>
<p class="text-sm text-amber-700 dark:text-amber-300 mb-3">
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="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" />
Apply SNMP Config to All Devices
{t("Apply SNMP Config to All Devices")}
</button>
</div>
<% end %>
@ -352,16 +352,16 @@
<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">
<div>
<h2 class="text-base/7 font-semibold text-gray-900 dark:text-white">
MikroTik API Configuration
{t("MikroTik API Configuration")}
</h2>
<p class="mt-1 text-sm/6 text-gray-500 dark:text-gray-400">
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">
<.icon name="hero-beaker" class="h-4 w-4 inline" />
<strong>Experimental Feature:</strong>
MikroTik API integration is under active development.
<strong>{t("Experimental Feature:")}</strong>
{t("MikroTik API integration is under active development.")}
</p>
</div>
<p class="mt-3 text-xs text-gray-500 dark:text-gray-400 italic">
@ -374,14 +374,14 @@
<.input
field={@form[:mikrotik_enabled]}
type="checkbox"
label="Enable MikroTik API"
label={t("Enable MikroTik API")}
/>
<div class={"#{unless @form[:mikrotik_enabled].value, do: "hidden"}"}>
<.input
field={@form[:mikrotik_username]}
type="text"
label="Username"
label={t("Username")}
placeholder="e.g., admin"
autocomplete="off"
/>
@ -391,8 +391,8 @@
<.input
field={@form[:mikrotik_password]}
type="password"
label="Password"
placeholder="RouterOS API password"
label={t("Password")}
placeholder={t("RouterOS API password")}
autocomplete="off"
/>
</div>
@ -401,7 +401,7 @@
<.input
field={@form[:mikrotik_port]}
type="number"
label="API Port"
label={t("API Port")}
placeholder="8729"
autocomplete="off"
/>
@ -411,15 +411,15 @@
<.input
field={@form[:mikrotik_use_ssl]}
type="checkbox"
label="Use SSL (API-SSL)"
label={t("Use SSL (API-SSL)")}
/>
</div>
<div class={"#{unless @form[:mikrotik_enabled].value, do: "hidden"} #{if @form[:mikrotik_use_ssl].value != false, do: "hidden"}"}>
<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>
Plain API (port 8728) sends credentials unencrypted over the network. This setting is
<strong>{t("Critical Security Warning:")}</strong>
{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>
@ -429,8 +429,8 @@
<div class={"#{unless @form[:mikrotik_enabled].value, do: "hidden"} #{if @form[:mikrotik_use_ssl].value == false, do: "hidden"}"}>
<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>Security Warning:</strong>
Plain API (port 8728) sends credentials unencrypted. Use SSL (port 8729) whenever possible.
<strong>{t("Security Warning:")}</strong>
{t("Plain API (port 8728) sends credentials unencrypted. Use SSL (port 8729) whenever possible.")}
</p>
</div>
</div>
@ -444,10 +444,10 @@
<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">
<div>
<h2 class="text-base/7 font-semibold text-gray-900 dark:text-white">
Default Agent
{t("Default Agent")}
</h2>
<p class="mt-1 text-sm/6 text-gray-500 dark:text-gray-400">
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" />
@ -459,14 +459,14 @@
<.input
field={@form[:default_agent_token_id]}
type="select"
label="Default Remote Agent"
label={t("Default Remote Agent")}
prompt="No default agent - cloud polling"
options={Enum.map(@available_agents, &{&1.name, &1.id})}
/>
<div class="rounded-md bg-gray-50 dark:bg-gray-800/50 p-4">
<p class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Current Device Assignment:
{t("Current Device Assignment:")}
</p>
<div class="space-y-1 text-sm text-gray-600 dark:text-gray-400">
<div class="flex items-center gap-2">
@ -500,19 +500,19 @@
<%= if @organization.default_agent_token_id do %>
<div class="mt-6 rounded-md bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 p-4">
<h3 class="text-sm font-medium text-amber-900 dark:text-amber-200 mb-2">
Force Apply to All Devices
{t("Force Apply to All Devices")}
</h3>
<p class="text-sm text-amber-700 dark:text-amber-300 mb-3">
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="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" />
Apply Default Agent to All Devices
{t("Apply Default Agent to All Devices")}
</button>
</div>
<% end %>
@ -522,15 +522,15 @@
<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">
<div>
<h2 class="text-base/7 font-semibold text-gray-900 dark:text-white">
Default Agent
{t("Default Agent")}
</h2>
<p class="mt-1 text-sm/6 text-gray-500 dark:text-gray-400">
No agents configured yet.
{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"
>
Create an agent
{t("Create an agent")}
</.link>
to enable remote polling.
</p>
@ -550,14 +550,14 @@
navigate={~p"/dashboard"}
class="text-sm font-semibold text-gray-900 dark:text-white"
>
Cancel
{t("Cancel")}
</.link>
<button
type="submit"
phx-disable-with="Saving..."
phx-disable-with={t("Saving...")}
class="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:shadow-none dark:hover:bg-indigo-400 dark:focus-visible:outline-indigo-500"
>
Save Settings
{t("Save Settings")}
</button>
</div>
</.form>
@ -570,10 +570,10 @@
<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">
<div>
<h2 class="text-base/7 font-semibold text-gray-900 dark:text-white">
Invite a Team Member
{t("Invite a Team Member")}
</h2>
<p class="mt-1 text-sm/6 text-gray-500 dark:text-gray-400">
Send an invitation email to add someone to this organization.
{t("Send an invitation email to add someone to this organization.")}
</p>
</div>
@ -583,22 +583,22 @@
<.input
field={@invite_form[:email]}
type="email"
label="Email address"
label={t("Email address")}
placeholder="colleague@example.com"
required
/>
<.input
field={@invite_form[:role]}
type="select"
label="Role"
label={t("Role")}
options={[{"Admin", "admin"}, {"Member", "member"}, {"Viewer", "viewer"}]}
/>
<button
type="submit"
phx-disable-with="Sending..."
phx-disable-with={t("Sending...")}
class="rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-xs hover:bg-indigo-500 dark:bg-indigo-500 dark:shadow-none dark:hover:bg-indigo-400 h-[42px]"
>
Send Invite
{t("Send Invite")}
</button>
</div>
</.form>
@ -611,10 +611,10 @@
<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">
<div>
<h2 class="text-base/7 font-semibold text-gray-900 dark:text-white">
Pending Invitations
{t("Pending Invitations")}
</h2>
<p class="mt-1 text-sm/6 text-gray-500 dark:text-gray-400">
Invitations that haven't been accepted yet.
{t("Invitations that haven't been accepted yet.")}
</p>
</div>
@ -624,20 +624,20 @@
<thead class="bg-gray-50 dark:bg-white/5">
<tr>
<th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">
Email
{t("Email")}
</th>
<th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">
Role
{t("Role")}
</th>
<th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">
Sent
{t("Sent")}
</th>
<th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">
Expires
{t("Expires")}
</th>
<%= if @membership.role in [:owner, :admin] do %>
<th class="px-4 py-3 text-right text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">
Actions
{t("Actions")}
</th>
<% end %>
</tr>
@ -672,10 +672,10 @@
type="button"
phx-click="cancel_invitation"
phx-value-id={invitation.id}
data-confirm="Cancel this invitation?"
data-confirm={t("Cancel this invitation?")}
class="text-red-600 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300"
>
Cancel
{t("Cancel")}
</button>
</td>
<% end %>
@ -691,10 +691,10 @@
<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">
<div>
<h2 class="text-base/7 font-semibold text-gray-900 dark:text-white">
Members
{t("Members")}
</h2>
<p class="mt-1 text-sm/6 text-gray-500 dark:text-gray-400">
People who have access to this organization.
{t("People who have access to this organization.")}
</p>
</div>
@ -704,17 +704,17 @@
<thead class="bg-gray-50 dark:bg-white/5">
<tr>
<th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">
User
{t("User")}
</th>
<th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">
Role
{t("Role")}
</th>
<th class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">
Joined
{t("Joined")}
</th>
<%= if @membership.role in [:owner, :admin] do %>
<th class="px-4 py-3 text-right text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400">
Actions
{t("Actions")}
</th>
<% end %>
</tr>
@ -765,13 +765,13 @@
class="rounded-md border-gray-300 py-1 pl-2 pr-8 text-xs dark:border-white/10 dark:bg-white/5 dark:text-white"
>
<option value="admin" selected={member.role == :admin}>
Admin
{t("Admin")}
</option>
<option value="member" selected={member.role == :member}>
Member
{t("Member")}
</option>
<option value="viewer" selected={member.role == :viewer}>
Viewer
{t("Viewer")}
</option>
</select>
</form>
@ -779,10 +779,10 @@
type="button"
phx-click="remove_member"
phx-value-user-id={member.user_id}
data-confirm="Remove this member from the organization?"
data-confirm={t("Remove this member from the organization?")}
class="text-red-600 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300"
>
Remove
{t("Remove")}
</button>
</div>
<% end %>
@ -936,7 +936,7 @@
/>
<div>
<p class="text-sm font-medium text-red-800 dark:text-red-300">
Last sync failed
{t("Last sync failed")}
</p>
<%= if integration.last_sync_message && integration.last_sync_message != "" do %>
<p class="mt-0.5 text-sm text-red-700 dark:text-red-400">
@ -975,7 +975,7 @@
<div class="flex items-center gap-1.5 text-sm text-gray-500 dark:text-gray-400">
<.icon name="hero-clock" class="h-3.5 w-3.5" />
<span>
Synced
{t("Synced")}
<.timestamp
datetime={integration.last_synced_at}
timezone={@timezone}
@ -985,7 +985,7 @@
</div>
<% else %>
<span class="text-sm italic text-gray-400 dark:text-gray-500">
Never synced
{t("Never synced")}
</span>
<% end %>
@ -1093,17 +1093,17 @@
<div class="flex items-center gap-2 mb-1">
<.icon name="hero-link" class="h-4 w-4 text-gray-400" />
<h4 class="text-sm font-semibold text-gray-900 dark:text-white">
Connection
{t("Connection")}
</h4>
</div>
<p class="text-xs text-gray-500 dark:text-gray-400 mb-4 ml-6">
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.
{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">
<div>
<label class="block text-sm font-semibold text-gray-900 dark:text-white mb-1">
NetBox URL
{t("NetBox URL")}
</label>
<input
type="url"
@ -1116,13 +1116,13 @@
</div>
<div>
<label class="block text-sm font-semibold text-gray-900 dark:text-white mb-1">
API Token
{t("API Token")}
</label>
<input
type="password"
name="integration[api_token]"
value={get_credential(@integrations["netbox"], "api_token")}
placeholder="Enter your NetBox API token"
placeholder={t("Enter your NetBox API token")}
autocomplete="off"
class="block w-full rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm text-gray-900 placeholder-gray-400 focus:border-indigo-500 focus:ring-indigo-500 dark:border-white/10 dark:bg-gray-800 dark:text-white dark:placeholder-gray-500"
/>
@ -1135,11 +1135,11 @@
<div class="flex items-center gap-2 mb-1">
<.icon name="hero-arrows-right-left" class="h-4 w-4 text-gray-400" />
<h4 class="text-sm font-semibold text-gray-900 dark:text-white">
Sync Direction
{t("Sync Direction")}
</h4>
</div>
<p class="text-xs text-gray-500 dark:text-gray-400 mb-4 ml-6">
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 +1167,7 @@
NetBox → TowerOps
</span>
<span class="mt-1.5 text-xs text-gray-500 dark:text-gray-400">
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 +1191,7 @@
TowerOps → NetBox
</span>
<span class="mt-1.5 text-xs text-gray-500 dark:text-gray-400">
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 +1217,7 @@
/> Bidirectional
</span>
<span class="mt-1.5 text-xs text-gray-500 dark:text-gray-400">
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>
@ -1229,11 +1229,11 @@
<div class="flex items-center gap-2 mb-1">
<.icon name="hero-circle-stack" class="h-4 w-4 text-gray-400" />
<h4 class="text-sm font-semibold text-gray-900 dark:text-white">
What to Sync
{t("What to Sync")}
</h4>
</div>
<p class="text-xs text-gray-500 dark:text-gray-400 mb-4 ml-6">
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">
@ -1251,10 +1251,10 @@
/>
<div>
<span class="text-sm font-medium text-gray-900 dark:text-white">
Devices
{t("Devices")}
</span>
<p class="text-xs text-gray-500 dark:text-gray-400">
Name, IP, role, platform, status, serial number
{t("Name, IP, role, platform, status, serial number")}
</p>
</div>
</label>
@ -1273,10 +1273,10 @@
/>
<div>
<span class="text-sm font-medium text-gray-900 dark:text-white">
Sites
{t("Sites")}
</span>
<p class="text-xs text-gray-500 dark:text-gray-400">
Site names, locations, addresses, and coordinates
{t("Site names, locations, addresses, and coordinates")}
</p>
</div>
</label>
@ -1295,10 +1295,10 @@
/>
<div>
<span class="text-sm font-medium text-gray-900 dark:text-white">
IP Addresses
{t("IP Addresses")}
</span>
<p class="text-xs text-gray-500 dark:text-gray-400">
IPAM data — assigned IPs, prefixes, VRFs
{t("IPAM data — assigned IPs, prefixes, VRFs")}
</p>
</div>
</label>
@ -1317,10 +1317,10 @@
/>
<div>
<span class="text-sm font-medium text-gray-900 dark:text-white">
Interfaces
{t("Interfaces")}
</span>
<p class="text-xs text-gray-500 dark:text-gray-400">
Physical and virtual interfaces, LAGs, connections
{t("Physical and virtual interfaces, LAGs, connections")}
</p>
</div>
</label>
@ -1332,19 +1332,19 @@
<div class="flex items-center gap-2 mb-1">
<.icon name="hero-funnel" class="h-4 w-4 text-gray-400" />
<h4 class="text-sm font-semibold text-gray-900 dark:text-white">
Filters
{t("Filters")}
</h4>
<span class="text-xs font-normal text-gray-400 dark:text-gray-500">
(optional)
</span>
</div>
<p class="text-xs text-gray-500 dark:text-gray-400 mb-4 ml-6">
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>
<label class="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
Device Role
{t("Device Role")}
</label>
<input
type="text"
@ -1356,7 +1356,7 @@
</div>
<div>
<label class="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
Site
{t("Site")}
</label>
<input
type="text"
@ -1368,7 +1368,7 @@
</div>
<div>
<label class="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
Tag
{t("Tag")}
</label>
<input
type="text"
@ -1387,7 +1387,7 @@
<.input
field={@integration_form[:sync_interval_minutes]}
type="number"
label="Sync interval (minutes)"
label={t("Sync interval (minutes)")}
min="5"
value={
if(@integrations["netbox"],
@ -1397,7 +1397,7 @@
}
/>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
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>
@ -1440,10 +1440,10 @@
</button>
<button
type="submit"
phx-disable-with="Saving..."
phx-disable-with={t("Saving...")}
class="rounded-lg bg-indigo-600 px-5 py-2.5 text-sm font-semibold text-white shadow-xs transition-colors hover:bg-indigo-500 dark:bg-indigo-500 dark:hover:bg-indigo-400"
>
Save Integration
{t("Save Integration")}
</button>
</div>
</div>
@ -1460,7 +1460,7 @@
<div class="flex items-center gap-2 mb-4">
<.icon name="hero-key" class="h-4 w-4 text-gray-400" />
<h4 class="text-sm font-semibold text-gray-900 dark:text-white">
Connection Settings
{t("Connection Settings")}
</h4>
</div>
@ -1491,22 +1491,22 @@
<div class="rounded-lg bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800/50 p-4">
<h4 class="flex items-center gap-1.5 text-xs font-semibold text-blue-900 dark:text-blue-300 mb-2">
<.icon name="hero-information-circle" class="h-4 w-4" />
Where to find your Integration Key
{t("Where to find your Integration Key")}
</h4>
<ol class="list-decimal list-inside space-y-1 text-xs text-blue-800 dark:text-blue-300 ml-5">
<li>
In PagerDuty, go to <strong>Services</strong>
In PagerDuty, go to <strong>{t("Services")}</strong>
→ select your service (or create one)
</li>
<li>Click the <strong>Integrations</strong> tab</li>
<li>Click the <strong>{t("Integrations")}</strong> tab</li>
<li>
Click <strong>Add Integration</strong>
→ select <strong>Events API v2</strong>
Click <strong>{t("Add Integration")}</strong>
→ select <strong>{t("Events API v2")}</strong>
</li>
<li>Copy the <strong>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">
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>
@ -1514,16 +1514,16 @@
<div class="flex items-center gap-2 mb-4">
<.icon name="hero-arrow-path" class="h-4 w-4 text-gray-400" />
<h4 class="text-sm font-semibold text-gray-900 dark:text-white">
Two-Way Sync (Webhooks)
{t("Two-Way Sync (Webhooks)")}
</h4>
</div>
<p class="text-xs text-gray-500 dark:text-gray-400 mb-3">
When someone resolves or acknowledges an incident directly in PagerDuty,
TowerOps will automatically update the corresponding alert.
{t("When someone resolves or acknowledges an incident directly in PagerDuty,")}
{t("TowerOps will automatically update the corresponding alert.")}
</p>
<div class="mb-3">
<label class="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">
Webhook URL
{t("Webhook URL")}
</label>
<div class="flex items-center gap-2">
<code class="text-xs bg-gray-100 dark:bg-gray-800 text-gray-800 dark:text-gray-200 px-2.5 py-1.5 rounded-md break-all">
@ -1534,7 +1534,7 @@
<.input
name="integration[webhook_secret]"
type="password"
label="Webhook Signing Secret"
label={t("Webhook Signing Secret")}
value={
if(@integrations[provider.id],
do:
@ -1543,23 +1543,23 @@
else: ""
)
}
placeholder="Optional — paste from PagerDuty webhook extension"
placeholder={t("Optional — paste from PagerDuty webhook extension")}
/>
<div class="rounded-lg bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800/50 p-3 mt-3">
<ol class="list-decimal list-inside space-y-1 text-xs text-blue-800 dark:text-blue-300 ml-1">
<li>
In PagerDuty, go to <strong>Integrations</strong>
In PagerDuty, go to <strong>{t("Integrations")}</strong>
→ <strong>Generic Webhooks (v3)</strong>
</li>
<li>
Add a subscription with the Webhook URL above
{t("Add a subscription with the Webhook URL above")}
</li>
<li>
Select events: <strong>incident.resolved</strong>
and <strong>incident.acknowledged</strong>
</li>
<li>
Copy the <strong>Signing Secret</strong> and paste it above
Copy the <strong>{t("Signing Secret")}</strong> and paste it above
</li>
</ol>
</div>
@ -1571,14 +1571,14 @@
<div class="flex items-center gap-2 mb-4">
<.icon name="hero-clock" class="h-4 w-4 text-gray-400" />
<h4 class="text-sm font-semibold text-gray-900 dark:text-white">
Sync Settings
{t("Sync Settings")}
</h4>
</div>
<div class="max-w-xs">
<.input
field={@integration_form[:sync_interval_minutes]}
type="number"
label="Sync interval (minutes)"
label={t("Sync interval (minutes)")}
min="5"
value={
if(@integrations[provider.id],
@ -1627,14 +1627,14 @@
phx-click="close_config"
class="rounded-lg px-4 py-2.5 text-sm font-semibold text-gray-600 transition-colors hover:text-gray-900 dark:text-gray-400 dark:hover:text-white"
>
Cancel
{t("Cancel")}
</button>
<button
type="submit"
phx-disable-with="Saving..."
phx-disable-with={t("Saving...")}
class="rounded-lg bg-indigo-600 px-5 py-2.5 text-sm font-semibold text-white shadow-xs transition-colors hover:bg-indigo-500 dark:bg-indigo-500 dark:hover:bg-indigo-400"
>
Save Integration
{t("Save Integration")}
</button>
</div>
</div>
@ -1648,17 +1648,17 @@
<div class="flex items-center gap-2 mb-1">
<.icon name="hero-globe-alt" class="h-4 w-4 text-gray-400" />
<h4 class="text-sm font-semibold text-gray-900 dark:text-white">
Webhook Configuration
{t("Webhook Configuration")}
</h4>
</div>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400 ml-6">
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">
<div>
<label class="block text-xs font-medium text-gray-700 dark:text-gray-300">
Webhook URL
{t("Webhook URL")}
</label>
<div class="mt-1 flex items-center gap-2">
<input
@ -1682,10 +1682,10 @@
<div>
<label class="block text-xs font-medium text-gray-700 dark:text-gray-300">
Webhook Secret
{t("Webhook Secret")}
</label>
<p class="mt-0.5 text-xs text-gray-500 dark:text-gray-400">
Paste the secret key generated by Gaiia when you create the webhook.
{t("Paste the secret key generated by Gaiia when you create the webhook.")}
</p>
<div class="mt-1 flex items-center gap-2">
<input
@ -1693,7 +1693,7 @@
type="password"
name="webhook_secret"
value={get_credential(@integrations["gaiia"], "webhook_secret")}
placeholder="Paste the secret from Gaiia"
placeholder={t("Paste the secret from Gaiia")}
phx-blur="save_webhook_secret"
class="block w-full rounded-md border-gray-300 py-1.5 font-mono text-sm text-gray-900 shadow-xs sm:leading-6 dark:border-white/10 dark:bg-white/5 dark:text-white"
/>
@ -1714,7 +1714,7 @@
<li>Save the webhook</li>
</ol>
<p class="mt-2.5 text-xs text-blue-600 dark:text-blue-400 ml-5">
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

@ -12,7 +12,7 @@ defmodule ToweropsWeb.OrgLive.Index do
{:ok,
socket
|> assign(:page_title, "Your Organizations")
|> assign(:page_title, t("Your Organizations"))
|> assign(:organizations, organizations)}
end

View file

@ -30,9 +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">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">
Get started by creating a new organization.
{t("Get started by creating a new organization.")}
</p>
<div class="mt-6">
<.button navigate={~p"/orgs/new"} variant="primary">

View file

@ -16,7 +16,7 @@ defmodule ToweropsWeb.OrgLive.New do
{:ok,
socket
|> assign(:page_title, "New Organization")
|> assign(:page_title, t("New Organization"))
|> assign(:form, to_form(changeset))
|> assign(:can_create_free, can_create_free)
|> assign(:free_org_count, free_count)}
@ -42,7 +42,7 @@ defmodule ToweropsWeb.OrgLive.New do
{:ok, _organization} ->
{:noreply,
socket
|> put_flash(:info, "Organization created successfully")
|> put_flash(:info, t("Organization created successfully"))
|> push_navigate(to: ~p"/dashboard")}
{:error, %Ecto.Changeset{} = changeset} ->

View file

@ -4,7 +4,7 @@
>
<.header>
{@page_title}
<:subtitle>Create a new organization to manage your sites and devices</:subtitle>
<:subtitle>{t("Create a new organization to manage your sites and devices")}</:subtitle>
</.header>
<%= if not @can_create_free do %>
@ -13,13 +13,13 @@
<.icon name="hero-exclamation-triangle" class="h-5 w-5 text-orange-400" />
<div class="ml-3">
<h3 class="text-sm font-medium text-orange-800 dark:text-orange-400">
Free organization limit reached
{t("Free organization limit reached")}
</h3>
<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"}.
To create additional organizations, you'll need to upgrade your existing organization to a paid plan.
{t("To create additional organizations, you'll need to upgrade your existing organization to a paid plan.")}
</p>
</div>
</div>
@ -28,11 +28,11 @@
<div class="max-w-2xl">
<.form for={@form} id="organization-form" phx-change="validate" phx-submit="save">
<.input field={@form[:name]} type="text" label="Organization Name" required />
<.input field={@form[:name]} type="text" label={t("Organization Name")} required />
<div class="flex gap-3 mt-6">
<.button phx-disable-with="Creating..." variant="primary">Create Organization</.button>
<.button navigate={~p"/orgs"}>Cancel</.button>
<.button phx-disable-with={t("Creating...")} variant="primary">Create Organization</.button>
<.button navigate={~p"/orgs"}>{t("Cancel")}</.button>
</div>
</.form>
</div>

View file

@ -35,7 +35,7 @@ defmodule ToweropsWeb.SiteLive.Form do
end
socket
|> assign(:page_title, "New Site")
|> assign(:page_title, t("New Site"))
|> assign(:site, %Site{})
|> assign(:form, to_form(changeset))
|> assign(:org_agent, org_agent)
@ -58,7 +58,7 @@ defmodule ToweropsWeb.SiteLive.Form do
end
socket
|> assign(:page_title, "Edit Site")
|> assign(:page_title, t("Edit Site"))
|> assign(:site, site)
|> assign(:form, to_form(changeset))
|> assign(:available_parent_sites, available_parent_sites)
@ -102,7 +102,7 @@ defmodule ToweropsWeb.SiteLive.Form do
{:noreply,
socket
|> assign(:form, to_form(changeset))
|> put_flash(:info, "Address geocoded successfully!")}
|> put_flash(:info, t("Address geocoded successfully!"))}
{:error, :no_api_key} ->
{:noreply,
@ -113,13 +113,13 @@ defmodule ToweropsWeb.SiteLive.Form do
)}
{:error, reason} when is_binary(reason) ->
{:noreply, put_flash(socket, :error, "Geocoding failed: #{reason}")}
{:noreply, put_flash(socket, :error, t("Geocoding failed: %{reason}", reason: reason))}
{:error, _} ->
{:noreply, put_flash(socket, :error, "Geocoding failed. Please check the address and try again.")}
{:noreply, put_flash(socket, :error, t("Geocoding failed. Please check the address and try again."))}
end
else
{:noreply, put_flash(socket, :error, "Please enter an address to geocode")}
{:noreply, put_flash(socket, :error, t("Please enter an address to geocode"))}
end
end

View file

@ -27,12 +27,12 @@
<div class="max-w-2xl">
<.form for={@form} id="site-form" phx-change="validate" phx-submit="save">
<.input field={@form[:name]} type="text" label="Site Name" required />
<.input field={@form[:name]} type="text" label={t("Site Name")} required />
<div class="mb-4">
<label>
<span class="block text-sm font-medium text-gray-900 dark:text-white mb-2">
Parent Site
{t("Parent Site")}
<span class="text-gray-400 dark:text-gray-500 font-normal">(optional)</span>
</span>
<select
@ -67,18 +67,18 @@
<!-- Geographic Location Section -->
<div class="mt-6 border-t pt-6">
<h3 class="text-base font-medium mb-4">
Geographic Location
{t("Geographic Location")}
<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">
Add geographic coordinates to enable network mapping and spatial analysis.
{t("Add geographic coordinates to enable network mapping and spatial analysis.")}
</p>
<div class="space-y-4">
<div>
<label>
<span class="block text-sm font-medium text-gray-900 dark:text-white mb-2">
Address
{t("Address")}
</span>
<div class="flex gap-2">
<input
@ -94,11 +94,11 @@
phx-click="geocode"
class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 dark:focus:ring-offset-gray-800"
>
Geocode
{t("Geocode")}
</button>
</div>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
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>
@ -107,7 +107,7 @@
<div>
<label>
<span class="block text-sm font-medium text-gray-900 dark:text-white mb-2">
Latitude
{t("Latitude")}
</span>
<input
type="number"
@ -129,7 +129,7 @@
<div>
<label>
<span class="block text-sm font-medium text-gray-900 dark:text-white mb-2">
Longitude
{t("Longitude")}
</span>
<input
type="number"
@ -153,7 +153,7 @@
<%= if @form[:latitude].value && @form[:longitude].value do %>
<div class="mt-4">
<span class="block text-sm font-medium text-gray-900 dark:text-white mb-2">
Map Preview
{t("Map Preview")}
</span>
<div class="border border-gray-300 dark:border-gray-600 rounded-lg overflow-hidden">
<img
@ -174,7 +174,7 @@
<div class="mb-4">
<label>
<span class="block text-sm font-medium text-gray-900 dark:text-white mb-2">
Description
{t("Description")}
<span class="text-gray-400 dark:text-gray-500 font-normal">(optional)</span>
</span>
<textarea
@ -188,17 +188,17 @@
<%= if @available_agents != [] do %>
<div class="mt-6 border-t pt-6">
<h3 class="text-base font-medium mb-4">
Agent Configuration
{t("Agent Configuration")}
<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">
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
field={@form[:agent_token_id]}
type="select"
label="Default Agent for Site"
label={t("Default Agent for Site")}
prompt="Inherit from organization"
options={Enum.map(@available_agents, &{&1.name, &1.id})}
/>
@ -206,21 +206,21 @@
<%= if @live_action == :edit and @site.agent_token_id do %>
<p class="mt-1 text-sm text-amber-600 dark:text-amber-400 flex items-center gap-1">
<.icon name="hero-exclamation-triangle" class="h-4 w-4" />
<strong>Overriding</strong> organization default -
<strong>{t("Overriding")}</strong> organization default -
all devices at this site will use this agent
</p>
<div class="mt-4 p-3 bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 rounded-lg">
<p class="text-sm font-medium text-amber-900 dark:text-amber-200 mb-2">
Force Apply to Site Device
{t("Force Apply to Site Device")}
</p>
<p class="text-sm text-amber-700 dark:text-amber-300 mb-3">
This will assign this agent to ALL devices at this site.
{t("This will assign this agent to ALL devices at this site.")}
</p>
<.button
type="button"
phx-click="apply_agent_to_all"
data-confirm="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 +234,7 @@
</p>
<% else %>
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400">
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 %>
@ -243,17 +243,17 @@
<div class="mt-6 border-t pt-6">
<h3 class="text-base font-medium mb-4">
SNMP Configuration
{t("SNMP Configuration")}
<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">
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
field={@form[:snmp_version]}
type="select"
label="SNMP Version"
label={t("SNMP Version")}
prompt="Inherit from organization"
options={[{"v1", "1"}, {"v2c", "2c"}, {"v3", "3"}]}
/>
@ -263,8 +263,8 @@
<.input
field={@form[:snmp_community]}
type="text"
label="SNMP Community String"
placeholder="Leave blank to inherit from organization"
label={t("SNMP Community String")}
placeholder={t("Leave blank to inherit from organization")}
/>
<% end %>
@ -273,7 +273,7 @@
<.input
field={@form[:snmpv3_security_level]}
type="select"
label="Security Level"
label={t("Security Level")}
prompt="Select security level"
options={[
{"No Auth, No Priv", "noAuthNoPriv"},
@ -285,8 +285,8 @@
<.input
field={@form[:snmpv3_username]}
type="text"
label="Username"
placeholder="Leave blank to inherit from organization"
label={t("Username")}
placeholder={t("Leave blank to inherit from organization")}
/>
<!-- Auth fields (shown for authNoPriv and authPriv) -->
@ -294,7 +294,7 @@
<.input
field={@form[:snmpv3_auth_protocol]}
type="select"
label="Auth Protocol"
label={t("Auth Protocol")}
prompt="Select protocol"
options={[
{"SHA-256 (recommended)", "SHA-256"},
@ -309,8 +309,8 @@
<.input
field={@form[:snmpv3_auth_password]}
type="password"
label="Auth Password"
placeholder="Min 8 characters"
label={t("Auth Password")}
placeholder={t("Min 8 characters")}
autocomplete="off"
/>
<% end %>
@ -320,7 +320,7 @@
<.input
field={@form[:snmpv3_priv_protocol]}
type="select"
label="Privacy Protocol"
label={t("Privacy Protocol")}
prompt="Select protocol"
options={[
{"AES-128 (recommended)", "AES"},
@ -334,8 +334,8 @@
<.input
field={@form[:snmpv3_priv_password]}
type="password"
label="Privacy Password"
placeholder="Min 8 characters"
label={t("Privacy Password")}
placeholder={t("Min 8 characters")}
autocomplete="off"
/>
<% end %>
@ -344,22 +344,22 @@
<.input
field={@form[:snmp_port]}
type="number"
label="SNMP Port"
placeholder="Leave blank to inherit from organization (default: 161)"
label={t("SNMP Port")}
placeholder={t("Leave blank to inherit from organization (default: 161)")}
/>
<%= if @live_action == :edit and @site.snmp_community do %>
<div class="mt-4 p-3 bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 rounded-lg">
<p class="text-sm font-medium text-amber-900 dark:text-amber-200 mb-2">
Force Apply to Site Device
{t("Force Apply to Site Device")}
</p>
<p class="text-sm text-amber-700 dark:text-amber-300 mb-3">
This will override SNMP settings for ALL devices at this site.
{t("This will override SNMP settings for ALL devices at this site.")}
</p>
<.button
type="button"
phx-click="apply_snmp_to_all"
data-confirm="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
@ -371,59 +371,59 @@
<%= if @current_scope.user.is_superuser do %>
<div class="mt-6 border-t pt-6">
<h3 class="text-base font-medium mb-4">
MikroTik API Configuration
{t("MikroTik API Configuration")}
<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">
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">
<.icon name="hero-beaker" class="h-4 w-4 inline" />
<strong>Experimental Feature:</strong>
MikroTik API integration is under active development.
<strong>{t("Experimental Feature:")}</strong>
{t("MikroTik API integration is under active development.")}
</p>
</div>
<.input
field={@form[:mikrotik_enabled]}
type="checkbox"
label="Enable MikroTik API"
label={t("Enable MikroTik API")}
/>
<%= if @form[:mikrotik_enabled].value do %>
<.input
field={@form[:mikrotik_username]}
type="text"
label="Username"
placeholder="Leave blank to inherit from organization"
label={t("Username")}
placeholder={t("Leave blank to inherit from organization")}
/>
<.input
field={@form[:mikrotik_password]}
type="password"
label="Password"
placeholder="Leave blank to inherit from organization"
label={t("Password")}
placeholder={t("Leave blank to inherit from organization")}
/>
<.input
field={@form[:mikrotik_port]}
type="number"
label="API Port"
label={t("API Port")}
placeholder="8729 (SSL) or 8728 (plain)"
/>
<.input
field={@form[:mikrotik_use_ssl]}
type="checkbox"
label="Use SSL (API-SSL)"
label={t("Use SSL (API-SSL)")}
/>
<%= if @form[:mikrotik_use_ssl].value == false do %>
<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>
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 +432,7 @@
<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>
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 %>
@ -441,20 +441,20 @@
<% end %>
<div class="flex gap-3 mt-6">
<.button phx-disable-with="Saving..." variant="primary">Save Site</.button>
<.button navigate={~p"/sites"}>Cancel</.button>
<.button phx-disable-with={t("Saving...")} variant="primary">Save Site</.button>
<.button navigate={~p"/sites"}>{t("Cancel")}</.button>
</div>
</.form>
<%= 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">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">
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="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

@ -18,7 +18,7 @@ defmodule ToweropsWeb.SiteLive.Index do
{:ok,
socket
|> assign(:page_title, "Sites")
|> assign(:page_title, t("Sites"))
|> assign(:sites, sites)
|> assign(:summary_map, summary_map)
|> assign(:insights, insights)
@ -40,15 +40,15 @@ defmodule ToweropsWeb.SiteLive.Index do
{:noreply,
socket
|> assign(:insights, insights)
|> put_flash(:info, "Insight dismissed")}
|> put_flash(:info, t("Insight dismissed"))}
{:error, _} ->
{:noreply, put_flash(socket, :error, "Failed to dismiss insight")}
{:noreply, put_flash(socket, :error, t("Failed to dismiss insight"))}
end
end
defp apply_action(socket, :index, _params) do
assign(socket, :page_title, "Sites")
assign(socket, :page_title, t("Sites"))
end
defp format_qoe(nil), do: ""

View file

@ -5,7 +5,7 @@
>
<.header>
{@page_title}
<:subtitle>Manage your site locations and hierarchy</:subtitle>
<:subtitle>{t("Manage your site locations and hierarchy")}</:subtitle>
<:actions>
<.link navigate={~p"/sites-map"} class="btn btn-outline btn-sm gap-1.5 mr-2">
<.icon name="hero-map" class="h-4 w-4" /> Map View
@ -23,9 +23,9 @@
<div class="rounded-full bg-blue-100 dark:bg-blue-900/40 p-4 mb-2">
<.icon name="hero-map-pin-solid" class="h-12 w-12 text-blue-500 dark:text-blue-400" />
</div>
<h3 class="card-title text-gray-900 dark:text-white">No Sites Yet</h3>
<h3 class="card-title text-gray-900 dark:text-white">{t("No Sites Yet")}</h3>
<p class="text-sm text-gray-600 dark:text-gray-400">
Create your first site to organize devices by tower location.
{t("Create your first site to organize devices by tower location.")}
</p>
<div class="card-actions mt-2">
<.button navigate={~p"/sites/new"} variant="primary">
@ -42,22 +42,22 @@
<thead class="bg-gray-50 dark:bg-gray-800/80">
<tr>
<th class="px-4 py-2 text-left text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400">
Site
{t("Site")}
</th>
<th class="px-4 py-2 text-right text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400">
Devices
{t("Devices")}
</th>
<th class="px-4 py-2 text-right text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400">
Down
{t("Down")}
</th>
<th class="px-4 py-2 text-right text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400">
QoE
{t("QoE")}
</th>
<th class="px-4 py-2 text-right text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400">
Subscribers
{t("Subscribers")}
</th>
<th class="px-4 py-2 text-left text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400">
Location
{t("Location")}
</th>
</tr>
</thead>
@ -120,7 +120,7 @@
<div class="mt-8">
<div class="flex items-center justify-between mb-3">
<h2 class="text-sm font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">
Insights
{t("Insights")}
</h2>
<.link
navigate={~p"/insights"}

View file

@ -16,7 +16,7 @@
<.button
type="button"
phx-click="force_rediscover_all"
data-confirm="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>
@ -86,7 +86,7 @@
<div class="flex items-center justify-between">
<div>
<h3 class="text-sm font-semibold text-gray-700 dark:text-gray-300">
Preseem QoE Score
{t("Preseem QoE Score")}
</h3>
<p class={["mt-1 text-3xl font-bold", qoe_color(@qoe_summary[:score])]}>
{if @qoe_summary[:score], do: Float.round(@qoe_summary[:score] / 1, 1), else: "—"}
@ -118,7 +118,7 @@
<%!-- Site Details --%>
<div class="card bg-base-100 border border-base-300 shadow-sm">
<div class="card-body p-4">
<h3 class="card-title text-base">Site Details</h3>
<h3 class="card-title text-base">{t("Site Details")}</h3>
<dl class="mt-2 space-y-3">
<%= if @site.parent_site do %>
<div>
@ -148,7 +148,7 @@
</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">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">
@ -258,10 +258,10 @@
class="mx-auto h-12 w-12 text-gray-400 dark:text-gray-500"
/>
<h4 class="mt-4 text-base font-semibold text-gray-900 dark:text-white">
Add your first device
{t("Add your first device")}
</h4>
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">
Start monitoring by adding network devices to this site.
{t("Start monitoring by adding network devices to this site.")}
</p>
<div class="mt-4">
<.button navigate={~p"/devices/new?site_id=#{@site.id}"} variant="primary">

View file

@ -16,7 +16,7 @@ defmodule ToweropsWeb.TraceLive.Index do
issues = []
issues =
if assigns.trace[:device] && assigns.trace.device.status == :down, do: ["Device is DOWN" | issues], else: issues
if assigns.trace[:device] && assigns.trace.device.status == :down, do: [t("Device is DOWN") | issues], else: issues
issues =
if assigns.trace[:qoe_metrics] && assigns.trace.qoe_metrics[:qoe_score] && assigns.trace.qoe_metrics.qoe_score < 50,
@ -70,7 +70,7 @@ defmodule ToweropsWeb.TraceLive.Index do
def mount(_params, _session, socket) do
{:ok,
socket
|> assign(:page_title, "Trace")
|> assign(:page_title, t("Trace"))
|> assign(:query, "")
|> assign(:results, [])
|> assign(:trace, nil)

View file

@ -18,7 +18,7 @@
type="text"
name="query"
value={@query}
placeholder="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
@ -72,7 +72,7 @@
<.icon name="hero-magnifying-glass" class="h-12 w-12 mx-auto mb-3 opacity-50" />
<p class="text-lg font-medium">No results found</p>
<p class="text-sm">
Try searching by customer name, IP address, account ID, or device name
{t("Try searching by customer name, IP address, account ID, or device name")}
</p>
</div>
@ -143,7 +143,7 @@
</div>
<% else %>
<p class="text-sm text-gray-500 dark:text-gray-400 italic">
No subscriber info available
{t("No subscriber info available")}
</p>
<% end %>
</div>
@ -201,7 +201,7 @@
class="mt-2 pt-2 border-t border-base-200 dark:border-white/10"
>
<div class="text-xs font-semibold uppercase tracking-wider text-gray-400 mb-1">
Preseem AP
{t("Preseem AP")}
</div>
<div class="text-sm text-gray-700 dark:text-gray-300">
{@trace.access_point.name}
@ -230,22 +230,22 @@
<%= if @trace.qoe_metrics do %>
<div class="grid grid-cols-2 gap-3">
<.metric_card
label="QoE Score"
label={t("QoE Score")}
value={format_score(@trace.qoe_metrics.qoe_score)}
quality={score_quality(@trace.qoe_metrics.qoe_score)}
/>
<.metric_card
label="Capacity"
label={t("Capacity")}
value={format_score(@trace.qoe_metrics.capacity_score)}
quality={score_quality(@trace.qoe_metrics.capacity_score)}
/>
<.metric_card
label="RF Score"
label={t("RF Score")}
value={format_score(@trace.qoe_metrics.rf_score)}
quality={score_quality(@trace.qoe_metrics.rf_score)}
/>
<.metric_card
label="Airtime"
label={t("Airtime")}
value={format_pct(@trace.qoe_metrics.airtime_utilization)}
quality={airtime_quality(@trace.qoe_metrics.airtime_utilization)}
/>
@ -253,7 +253,7 @@
<%= if @trace.qoe_metrics.latest_metric do %>
<div class="mt-4 pt-3 border-t border-base-200 dark:border-white/10">
<div class="text-xs font-semibold uppercase tracking-wider text-gray-400 mb-2">
Latest Metrics
{t("Latest Metrics")}
</div>
<div class="grid grid-cols-2 gap-2 text-sm">
<div class="flex justify-between">
@ -294,7 +294,7 @@
<% end %>
<% else %>
<p class="text-sm text-gray-500 dark:text-gray-400 italic">
No Preseem data available
{t("No Preseem data available")}
</p>
<% end %>
</div>
@ -310,7 +310,7 @@
<div class="space-y-3">
<div class="flex items-center justify-between">
<span class="text-sm text-gray-500 dark:text-gray-400">
Subscribers on this AP
{t("Subscribers on this AP")}
</span>
<span class="text-2xl font-bold text-gray-900 dark:text-white">
{@trace.peer_impact.subscriber_count}
@ -318,7 +318,7 @@
</div>
<div class="flex items-center justify-between">
<span class="text-sm text-gray-500 dark:text-gray-400">
Other APs on device
{t("Other APs on device")}
</span>
<span class="text-lg font-semibold text-gray-700 dark:text-gray-300">
{@trace.peer_impact.peer_ap_count}
@ -326,7 +326,7 @@
</div>
<div class="flex items-center justify-between">
<span class="text-sm text-gray-500 dark:text-gray-400">
Active alerts on device
{t("Active alerts on device")}
</span>
<span class={[
"text-lg font-semibold",
@ -343,7 +343,7 @@
class="mt-3 pt-3 border-t border-base-200 dark:border-white/10"
>
<div class="text-xs font-semibold uppercase tracking-wider text-gray-400 mb-2">
Peer APs
{t("Peer APs")}
</div>
<div class="space-y-1">
<div
@ -366,7 +366,7 @@
</div>
<% else %>
<p class="text-sm text-gray-500 dark:text-gray-400 italic">
No peer data available
{t("No peer data available")}
</p>
<% end %>
</div>
@ -534,9 +534,9 @@
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">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">
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

@ -99,7 +99,7 @@ defmodule ToweropsWeb.UserRegistrationLive do
{:noreply,
socket
|> put_flash(:info, "Account created! Please check your email to verify your account.")
|> put_flash(:info, t("Account created! Please check your email to verify your account."))
|> redirect(to: ~p"/users/log-in?email=#{user.email}")
|> assign(:trigger_action, true)}
@ -122,14 +122,14 @@ defmodule ToweropsWeb.UserRegistrationLive do
{:noreply,
socket
|> put_flash(:info, "Account created! Please check your email to verify your account.")
|> put_flash(:info, t("Account created! Please check your email to verify your account."))
|> redirect(to: ~p"/users/log-in?email=#{user.email}")
|> assign(:trigger_action, true)}
else
{:error, :invalid_invitation} ->
{:noreply,
socket
|> put_flash(:error, "Invalid or expired invitation link.")
|> put_flash(:error, t("Invalid or expired invitation link."))
|> assign(:invitation, nil)
|> assign(:invitation_token, nil)}

View file

@ -12,7 +12,7 @@ defmodule ToweropsWeb.UserResetPasswordLive do
nil ->
{:ok,
socket
|> put_flash(:error, "Reset password link is invalid or has expired.")
|> put_flash(:error, t("Reset password link is invalid or has expired."))
|> redirect(to: ~p"/users/log-in")}
user ->
@ -56,7 +56,7 @@ defmodule ToweropsWeb.UserResetPasswordLive do
{:ok, {updated_user, _expired_tokens}} ->
{:noreply,
socket
|> put_flash(:info, "Password reset successfully.")
|> put_flash(:info, t("Password reset successfully."))
|> redirect(to: ~p"/users/log-in?email=#{updated_user.email}")}
{:error, changeset} ->

View file

@ -20,7 +20,7 @@ defmodule ToweropsWeb.UserSettingsLive do
socket =
socket
|> assign(:page_title, "Account Settings")
|> assign(:page_title, t("Account Settings"))
|> assign(:current_token, current_token)
|> assign_changesets()
|> assign_profile_form()
@ -107,7 +107,7 @@ defmodule ToweropsWeb.UserSettingsLive do
socket =
socket
|> assign(:current_scope, updated_scope)
|> put_flash(:info, "Profile updated successfully.")
|> put_flash(:info, t("Profile updated successfully."))
|> assign_profile_form()
{:noreply, socket}
@ -132,7 +132,7 @@ defmodule ToweropsWeb.UserSettingsLive do
socket =
socket
|> put_flash(:info, "A link to confirm your email change has been sent to the new address.")
|> put_flash(:info, t("A link to confirm your email change has been sent to the new address."))
|> assign_changesets()
{:noreply, socket}
@ -167,7 +167,7 @@ defmodule ToweropsWeb.UserSettingsLive do
# Redirect to re-authenticate with new password
socket =
socket
|> put_flash(:info, "Password updated successfully. Please log in with your new password.")
|> put_flash(:info, t("Password updated successfully. Please log in with your new password."))
|> redirect(to: ~p"/users/log-in")
{:noreply, socket}

View file

@ -4,10 +4,10 @@
>
<div class="pb-5">
<h1 class="text-3xl font-semibold tracking-tight text-gray-900 dark:text-white">
Account Settings
{t("Account Settings")}
</h1>
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
Manage your account email address, password, and security settings
{t("Manage your account email address, password, and security settings")}
</p>
</div>
@ -26,7 +26,7 @@
else: ""
}
>
Personal
{t("Personal")}
</.link>
</li>
<li>
@ -38,7 +38,7 @@
else: ""
}
>
Account
{t("Account")}
</.link>
</li>
<li>
@ -50,7 +50,7 @@
else: ""
}
>
Sessions
{t("Sessions")}
</.link>
</li>
<li>
@ -62,7 +62,7 @@
else: ""
}
>
API
{t("API")}
</.link>
</li>
<li>
@ -74,7 +74,7 @@
else: ""
}
>
Notifications
{t("Notifications")}
</.link>
</li>
<li>
@ -86,7 +86,7 @@
else: ""
}
>
Security
{t("Security")}
</.link>
</li>
<li>
@ -94,7 +94,7 @@
navigate={~p"/users/my-data"}
class="text-gray-500 hover:text-indigo-600 dark:text-gray-400 dark:hover:text-indigo-400"
>
My Data
{t("My Data")}
</.link>
</li>
</ul>
@ -107,10 +107,10 @@
<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">
Personal Information
{t("Personal Information")}
</h2>
<p class="mt-1 text-sm/6 text-gray-500 dark:text-gray-400">
Update your name and timezone preferences.
{t("Update your name and timezone preferences.")}
</p>
</div>
@ -126,7 +126,7 @@
for="first_name"
class="block text-sm/6 font-medium text-gray-900 dark:text-white"
>
First name
{t("First name")}
</label>
<div class="mt-2">
<input
@ -145,7 +145,7 @@
for="last_name"
class="block text-sm/6 font-medium text-gray-900 dark:text-white"
>
Last name
{t("Last name")}
</label>
<div class="mt-2">
<input
@ -164,7 +164,7 @@
for="timezone"
class="block text-sm/6 font-medium text-gray-900 dark:text-white"
>
Timezone
{t("Timezone")}
</label>
<div class="mt-2 grid grid-cols-1">
<select
@ -173,67 +173,67 @@
class="col-start-1 row-start-1 w-full appearance-none rounded-md bg-white py-1.5 pr-8 pl-3 text-base text-gray-900 outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6 dark:bg-white/5 dark:text-white dark:outline-white/10 dark:*:bg-gray-800 dark:focus:outline-indigo-500"
>
<option value="UTC" selected={@profile_form[:timezone].value == "UTC"}>
UTC (Coordinated Universal Time)
{t("UTC (Coordinated Universal Time)")}
</option>
<option
value="America/New_York"
selected={@profile_form[:timezone].value == "America/New_York"}
>
Eastern Time (US & Canada)
{t("Eastern Time (US & Canada)")}
</option>
<option
value="America/Chicago"
selected={@profile_form[:timezone].value == "America/Chicago"}
>
Central Time (US & Canada)
{t("Central Time (US & Canada)")}
</option>
<option
value="America/Denver"
selected={@profile_form[:timezone].value == "America/Denver"}
>
Mountain Time (US & Canada)
{t("Mountain Time (US & Canada)")}
</option>
<option
value="America/Los_Angeles"
selected={@profile_form[:timezone].value == "America/Los_Angeles"}
>
Pacific Time (US & Canada)
{t("Pacific Time (US & Canada)")}
</option>
<option
value="Europe/London"
selected={@profile_form[:timezone].value == "Europe/London"}
>
London
{t("London")}
</option>
<option
value="Europe/Paris"
selected={@profile_form[:timezone].value == "Europe/Paris"}
>
Paris
{t("Paris")}
</option>
<option
value="Europe/Berlin"
selected={@profile_form[:timezone].value == "Europe/Berlin"}
>
Berlin
{t("Berlin")}
</option>
<option
value="Asia/Tokyo"
selected={@profile_form[:timezone].value == "Asia/Tokyo"}
>
Tokyo
{t("Tokyo")}
</option>
<option
value="Asia/Shanghai"
selected={@profile_form[:timezone].value == "Asia/Shanghai"}
>
Shanghai
{t("Shanghai")}
</option>
<option
value="Australia/Sydney"
selected={@profile_form[:timezone].value == "Australia/Sydney"}
>
Sydney
{t("Sydney")}
</option>
</select>
<svg
@ -257,7 +257,7 @@
for="time_format"
class="block text-sm/6 font-medium text-gray-900 dark:text-white"
>
Time Format
{t("Time Format")}
</label>
<div class="mt-2 grid grid-cols-1">
<select
@ -293,7 +293,7 @@
for="language"
class="block text-sm/6 font-medium text-gray-900 dark:text-white"
>
Language
{t("Language")}
</label>
<div class="mt-2 grid grid-cols-1">
<select
@ -302,7 +302,7 @@
class="col-start-1 row-start-1 w-full appearance-none rounded-md bg-white py-1.5 pr-8 pl-3 text-base text-gray-900 outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400 focus:outline-2 focus:-outline-offset-2 focus:outline-indigo-600 sm:text-sm/6 dark:bg-white/5 dark:text-white dark:outline-white/10 dark:*:bg-gray-800 dark:focus:outline-indigo-500"
>
<option value="en" selected={@profile_form[:language].value == "en"}>
English
{t("English")}
</option>
</select>
<svg
@ -326,10 +326,10 @@
for="default_organization_id"
class="block text-sm/6 font-medium text-gray-900 dark:text-white"
>
Default Organization
{t("Default Organization")}
</label>
<p class="mt-1 text-sm/6 text-gray-500 dark:text-gray-400">
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
@ -367,10 +367,10 @@
<div class="mt-8 flex">
<button
type="submit"
phx-disable-with="Saving..."
phx-disable-with={t("Saving...")}
class="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:shadow-none dark:hover:bg-indigo-400 dark:focus-visible:outline-indigo-500"
>
Save
{t("Save")}
</button>
</div>
</.form>
@ -382,9 +382,9 @@
<!-- 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">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">
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>
@ -397,7 +397,7 @@
<div class="grid grid-cols-1 gap-x-6 gap-y-8 sm:max-w-xl sm:grid-cols-6">
<div class="col-span-full">
<label for="email" class="block text-sm/6 font-medium text-gray-900 dark:text-white">
Email address
{t("Email address")}
</label>
<div class="mt-2">
<input
@ -416,10 +416,10 @@
<div class="mt-8 flex">
<button
type="submit"
phx-disable-with="Changing..."
phx-disable-with={t("Changing...")}
class="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:shadow-none dark:hover:bg-indigo-400 dark:focus-visible:outline-indigo-500"
>
Change Email
{t("Change Email")}
</button>
</div>
</.form>
@ -429,10 +429,10 @@
<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">
Change Password
{t("Change Password")}
</h2>
<p class="mt-1 text-sm/6 text-gray-500 dark:text-gray-400">
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>
@ -448,7 +448,7 @@
for="new-password"
class="block text-sm/6 font-medium text-gray-900 dark:text-white"
>
New password
{t("New password")}
</label>
<div class="mt-2">
<input
@ -470,7 +470,7 @@
</div>
<div class="ml-3">
<h3 class="text-sm font-medium text-yellow-800 dark:text-yellow-200">
Password found in data breaches
{t("Password found in data breaches")}
</h3>
<div class="mt-2 text-sm text-yellow-700 dark:text-yellow-300">
<p>
@ -490,7 +490,7 @@
for="confirm-password"
class="block text-sm/6 font-medium text-gray-900 dark:text-white"
>
Confirm password
{t("Confirm password")}
</label>
<div class="mt-2">
<input
@ -508,10 +508,10 @@
<div class="mt-8 flex">
<button
type="submit"
phx-disable-with="Changing..."
phx-disable-with={t("Changing...")}
class="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:shadow-none dark:hover:bg-indigo-400 dark:focus-visible:outline-indigo-500"
>
Save Password
{t("Save Password")}
</button>
</div>
</.form>
@ -523,9 +523,9 @@
<!-- 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">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">
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>
@ -534,10 +534,10 @@
<div class="text-center rounded-lg border border-dashed border-gray-300 px-6 py-10 dark:border-gray-700">
<.icon name="hero-code-bracket" class="mx-auto h-12 w-12 text-gray-400" />
<h3 class="mt-2 text-sm font-semibold text-gray-900 dark:text-white">
No API tokens
{t("No API tokens")}
</h3>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
Get started by creating a new API token.
{t("Get started by creating a new API token.")}
</p>
<div class="mt-6">
<button
@ -571,7 +571,7 @@
@current_scope.timezone
)}
<% else %>
Never used
{t("Never used")}
<% end %>
</p>
</div>
@ -580,10 +580,10 @@
type="button"
phx-click="delete_api_token"
phx-value-token-id={token.id}
data-confirm="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"
>
Delete
{t("Delete")}
</button>
</li>
<% end %>
@ -609,10 +609,10 @@
<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">
Mobile Devices
{t("Mobile Devices")}
</h2>
<p class="mt-1 text-sm/6 text-gray-500 dark:text-gray-400">
Manage mobile devices that receive push notifications for alerts.
{t("Manage mobile devices that receive push notifications for alerts.")}
</p>
</div>
@ -621,10 +621,10 @@
<div class="text-center rounded-lg border border-dashed border-gray-300 px-6 py-10 dark:border-gray-700">
<.icon name="hero-device-phone-mobile" class="mx-auto h-12 w-12 text-gray-400" />
<h3 class="mt-2 text-sm font-semibold text-gray-900 dark:text-white">
No mobile devices
{t("No mobile devices")}
</h3>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
Add a mobile device to receive push notifications for alerts.
{t("Add a mobile device to receive push notifications for alerts.")}
</p>
<div class="mt-6">
<.link
@ -675,16 +675,16 @@
phx-value-enabled={if session.alerts_enabled, do: "false", else: "true"}
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"
>
Toggle
{t("Toggle")}
</button>
<button
type="button"
phx-click="revoke_mobile_device"
phx-value-session-id={session.id}
data-confirm="Are you sure you want to remove this device?"
data-confirm={t("Are you sure you want to remove this device?")}
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"
>
Remove
{t("Remove")}
</button>
</div>
</li>
@ -716,12 +716,12 @@
</div>
<div class="ml-3">
<h3 class="text-sm font-medium text-amber-800 dark:text-amber-200">
Security Alert
{t("Security Alert")}
</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.
Please review your login history below and ensure all activity is authorized.
{t("Please review your login history below and ensure all activity is authorized.")}
</p>
</div>
</div>
@ -734,10 +734,10 @@
<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">
Active Sessions
{t("Active Sessions")}
</h2>
<p class="mt-1 text-sm/6 text-gray-500 dark:text-gray-400">
Manage browser sessions where you're currently logged in.
{t("Manage browser sessions where you're currently logged in.")}
</p>
</div>
@ -746,10 +746,10 @@
<div class="text-center rounded-lg border border-dashed border-gray-300 px-6 py-10 dark:border-gray-700">
<.icon name="hero-computer-desktop" class="mx-auto h-12 w-12 text-gray-400" />
<h3 class="mt-2 text-sm font-semibold text-gray-900 dark:text-white">
No active sessions
{t("No active sessions")}
</h3>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
Your current session will appear here after your next login.
{t("Your current session will appear here after your next login.")}
</p>
</div>
<% else %>
@ -764,7 +764,7 @@
</p>
<%= if is_current do %>
<span class="inline-flex items-center rounded-md bg-green-50 px-2 py-1 text-xs font-medium text-green-700 inset-ring-1 inset-ring-green-600/20 dark:bg-green-500/10 dark:text-green-400 dark:inset-ring-green-500/20">
Current Session
{t("Current Session")}
</span>
<% end %>
</div>
@ -799,7 +799,7 @@
end
}
>
Revoke
{t("Revoke")}
</button>
</li>
<% end %>
@ -824,10 +824,10 @@
<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 border-t border-gray-200 dark:border-white/10">
<div>
<h2 class="text-base/7 font-semibold text-gray-900 dark:text-white">
Login History
{t("Login History")}
</h2>
<p class="mt-1 text-sm/6 text-gray-500 dark:text-gray-400">
Recent login attempts to your account, including successful and failed attempts.
{t("Recent login attempts to your account, including successful and failed attempts.")}
</p>
</div>
@ -836,10 +836,10 @@
<div class="text-center rounded-lg border border-dashed border-gray-300 px-6 py-10 dark:border-gray-700">
<.icon name="hero-clock" class="mx-auto h-12 w-12 text-gray-400" />
<h3 class="mt-2 text-sm font-semibold text-gray-900 dark:text-white">
No login history
{t("No login history")}
</h3>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
Your login attempts will appear here.
{t("Your login attempts will appear here.")}
</p>
</div>
<% else %>
@ -851,31 +851,31 @@
scope="col"
class="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 dark:text-white sm:pl-6"
>
Date & Time
{t("Date & Time")}
</th>
<th
scope="col"
class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 dark:text-white"
>
Status
{t("Status")}
</th>
<th
scope="col"
class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 dark:text-white"
>
Method
{t("Method")}
</th>
<th
scope="col"
class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 dark:text-white"
>
Location
{t("Location")}
</th>
<th
scope="col"
class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 dark:text-white"
>
IP Address
{t("IP Address")}
</th>
</tr>
</thead>
@ -929,7 +929,7 @@
}
class="relative inline-flex items-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 dark:border-white/10 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700"
>
Previous
{t("Previous")}
</.link>
<.link
:if={@login_history_pagination.page < @login_history_pagination.total_pages}
@ -938,7 +938,7 @@
}
class="relative ml-3 inline-flex items-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 dark:border-white/10 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700"
>
Next
{t("Next")}
</.link>
</div>
@ -946,7 +946,7 @@
<div class="hidden sm:flex sm:flex-1 sm:items-center sm:justify-between">
<div>
<p class="text-sm text-gray-700 dark:text-gray-400">
Showing
{t("Showing")}
<span class="font-medium">
{(@login_history_pagination.page - 1) * @login_history_pagination.per_page +
1}
@ -965,7 +965,7 @@
<div>
<nav
class="isolate inline-flex -space-x-px rounded-md shadow-sm"
aria-label="Pagination"
aria-label={t("Pagination")}
>
<!-- Previous button -->
<.link
@ -1029,10 +1029,10 @@
<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">
Authenticator Apps
{t("Authenticator Apps")}
</h2>
<p class="mt-1 text-sm/6 text-gray-500 dark:text-gray-400">
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>
@ -1041,10 +1041,10 @@
<div class="text-center rounded-lg border border-dashed border-gray-300 px-6 py-10 dark:border-gray-700">
<.icon name="hero-device-phone-mobile" class="mx-auto h-12 w-12 text-gray-400" />
<h3 class="mt-2 text-sm font-semibold text-gray-900 dark:text-white">
No authenticator apps
{t("No authenticator apps")}
</h3>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
Add your first authenticator app to enable two-factor authentication.
{t("Add your first authenticator app to enable two-factor authentication.")}
</p>
<div class="mt-6">
<button
@ -1101,7 +1101,7 @@
end
}
>
Remove
{t("Remove")}
</button>
</li>
<% end %>
@ -1124,10 +1124,10 @@
<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 border-t border-gray-200 dark:border-white/10">
<div>
<h2 class="text-base/7 font-semibold text-gray-900 dark:text-white">
Recovery Codes
{t("Recovery Codes")}
</h2>
<p class="mt-1 text-sm/6 text-gray-500 dark:text-gray-400">
Single-use backup codes for account access if you lose your authenticator app.
{t("Single-use backup codes for account access if you lose your authenticator app.")}
</p>
</div>
@ -1136,16 +1136,16 @@
<div class="flex items-center justify-between">
<div class="flex-1">
<h3 class="text-sm font-semibold text-gray-900 dark:text-white">
Available Recovery Codes
{t("Available Recovery Codes")}
</h3>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
<%= if @unused_recovery_codes_count == 0 do %>
<span class="text-red-600 dark:text-red-400 font-medium">
No recovery codes available
{t("No recovery codes available")}
</span>
- Generate new codes immediately
<% else %>
You have
{t("You have")}
<span class="font-medium text-gray-900 dark:text-white">
{@unused_recovery_codes_count}
</span>
@ -1157,7 +1157,7 @@
<button
type="button"
phx-click="regenerate_recovery_codes"
data-confirm="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
@ -1174,11 +1174,11 @@
</div>
<div class="ml-3">
<h3 class="text-sm font-medium text-amber-800 dark:text-amber-200">
No Recovery Codes
{t("No Recovery Codes")}
</h3>
<div class="mt-2 text-sm text-amber-700 dark:text-amber-300">
<p>
You have no recovery codes available. If you lose access to your authenticator app,
{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>
@ -1226,11 +1226,11 @@
class="text-lg font-semibold leading-6 text-gray-900 dark:text-white"
id="revoke-all-modal-title"
>
Revoke All Other Sessions?
{t("Revoke All Other Sessions?")}
</h3>
<div class="mt-2">
<p class="text-sm text-gray-500 dark:text-gray-400">
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>
@ -1241,14 +1241,14 @@
phx-click="confirm_revoke_all"
class="inline-flex w-full justify-center rounded-lg bg-red-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-red-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-600 sm:col-start-2"
>
Revoke All
{t("Revoke All")}
</button>
<button
type="button"
phx-click="cancel_revoke_all"
class="mt-3 inline-flex w-full justify-center rounded-lg bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50 dark:bg-gray-800/50 dark:text-white dark:ring-white/10 dark:hover:bg-gray-800 sm:col-start-1 sm:mt-0"
>
Cancel
{t("Cancel")}
</button>
</div>
</div>
@ -1289,11 +1289,11 @@
class="text-lg font-semibold leading-6 text-gray-900 dark:text-white"
id="create-api-token-modal-title"
>
Create API Token
{t("Create API Token")}
</h3>
<div class="mt-2">
<p class="text-sm text-gray-500 dark:text-gray-400">
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">
@ -1336,14 +1336,14 @@
type="submit"
class="inline-flex w-full justify-center rounded-lg bg-blue-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-blue-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600 disabled:opacity-50 disabled:cursor-not-allowed sm:col-start-2"
>
Create Token
{t("Create Token")}
</button>
<button
type="button"
phx-click="cancel_add_token"
class="mt-3 inline-flex w-full justify-center rounded-lg bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50 dark:bg-gray-800/50 dark:text-white dark:ring-white/10 dark:hover:bg-gray-800 sm:col-start-1 sm:mt-0"
>
Cancel
{t("Cancel")}
</button>
</div>
</.form>
@ -1385,11 +1385,11 @@
id="token-success-modal-title"
class="text-lg font-semibold leading-6 text-gray-900 dark:text-white"
>
API Token Created
{t("API Token Created")}
</h3>
<div class="mt-2">
<p class="text-sm text-gray-500 dark:text-gray-400">
Copy this token now. You won't be able to see it again.
{t("Copy this token now. You won't be able to see it again.")}
</p>
</div>
<div class="mt-4">
@ -1410,11 +1410,11 @@
class="absolute right-2 top-1/2 -translate-y-1/2 px-3 py-1.5 text-xs font-medium text-white bg-blue-600 hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600 rounded shadow-sm transition-colors flex items-center gap-1.5"
>
<.icon name="hero-clipboard" class="h-3.5 w-3.5" />
<span>Copy</span>
<span>{t("Copy")}</span>
</button>
</div>
<p class="mt-1.5 text-xs text-gray-500 dark:text-gray-400">
Click the token to select all, or use the Copy button
{t("Click the token to select all, or use the Copy button")}
</p>
</div>
</div>
@ -1425,7 +1425,7 @@
phx-click="close_token_modal"
class="inline-flex w-full justify-center rounded-lg bg-blue-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-blue-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600"
>
Done
{t("Done")}
</button>
</div>
</div>
@ -1469,7 +1469,7 @@
class="text-lg font-semibold leading-6 text-gray-900 dark:text-white"
id="add-device-modal-title"
>
Add Authenticator Device
{t("Add Authenticator Device")}
</h3>
<div class="mt-2">
<p class="text-sm text-gray-500 dark:text-gray-400">
@ -1493,14 +1493,14 @@
type="submit"
class="inline-flex w-full justify-center rounded-lg bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 sm:col-start-2"
>
Continue
{t("Continue")}
</button>
<button
type="button"
phx-click="cancel_add_device"
class="mt-3 inline-flex w-full justify-center rounded-lg bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50 dark:bg-gray-800/50 dark:text-white dark:ring-white/10 dark:hover:bg-gray-800 sm:col-start-1 sm:mt-0"
>
Cancel
{t("Cancel")}
</button>
</div>
</.form>
@ -1541,11 +1541,11 @@
class="text-lg font-semibold leading-6 text-gray-900 dark:text-white"
id="device-qr-modal-title"
>
Scan QR Code
{t("Scan QR Code")}
</h3>
<div class="mt-2">
<p class="text-sm text-gray-500 dark:text-gray-400">
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>
@ -1559,7 +1559,7 @@
<!-- Verification Code Input -->
<div class="mt-6">
<p class="mb-2 text-sm font-medium text-gray-900 dark:text-white">
Enter the 6-digit code from your app to verify
{t("Enter the 6-digit code from your app to verify")}
</p>
<.form for={%{}} as={:verification} phx-submit="verify_new_device">
<div class="flex justify-center gap-3">
@ -1579,7 +1579,7 @@
type="submit"
class="inline-flex items-center gap-x-1.5 rounded-md bg-indigo-600 px-4 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"
>
Verify
{t("Verify")}
</button>
</div>
</.form>
@ -1592,7 +1592,7 @@
phx-click="close_device_qr_modal"
class="inline-flex w-full justify-center rounded-lg bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50 dark:bg-gray-800/50 dark:text-white dark:ring-white/10 dark:hover:bg-gray-800"
>
Cancel
{t("Cancel")}
</button>
</div>
</div>
@ -1631,11 +1631,11 @@
class="text-lg font-semibold leading-6 text-gray-900 dark:text-white"
id="recovery-codes-modal-title"
>
Recovery Codes Generated
{t("Recovery Codes Generated")}
</h3>
<div class="mt-2">
<p class="text-sm text-gray-500 dark:text-gray-400">
Save these codes in a secure location. Each code can only be used once.
{t("Save these codes in a secure location. Each code can only be used once.")}
</p>
</div>
@ -1680,12 +1680,12 @@
</div>
<div class="ml-3">
<h3 class="text-sm font-medium text-amber-800 dark:text-amber-200">
Important
{t("Important")}
</h3>
<div class="mt-2 text-sm text-amber-700 dark:text-amber-300">
<p>
These codes won't be shown again. Store them securely (password manager, encrypted file, etc.).
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>
@ -1699,7 +1699,7 @@
phx-click="close_recovery_codes_modal"
class="inline-flex w-full justify-center rounded-lg bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
>
I've Saved These Codes
{t("I've Saved These Codes")}
</button>
</div>
</div>

View file

@ -13,7 +13,7 @@ defmodule ToweropsWeb.Permissions do
if can?(socket, :delete, :backup) do
# Delete backup
else
{:noreply, put_flash(socket, :error, "You don't have permission to delete backups")}
{:noreply, put_flash(socket, :error, t("You don't have permission to delete backups"))}
end
end
@ -32,12 +32,14 @@ defmodule ToweropsWeb.Permissions do
# Delete backup
else
conn
|> put_flash(:error, "You don't have permission to delete backups")
|> put_flash(:error, t("You don't have permission to delete backups"))
|> redirect(to: ~p"/")
end
end
"""
import ToweropsWeb.GettextHelpers
alias Phoenix.LiveView.Socket
alias Towerops.Accounts.Scope
alias Towerops.Organizations.Policy

View file

@ -12,6 +12,7 @@ defmodule ToweropsWeb.Plugs.UpdateSessionActivity do
"""
import Phoenix.Controller, only: [put_flash: 3, redirect: 2]
import Plug.Conn
import ToweropsWeb.GettextHelpers
alias Towerops.Accounts
@ -94,7 +95,7 @@ defmodule ToweropsWeb.Plugs.UpdateSessionActivity do
|> configure_session(renew: true)
|> clear_session()
|> delete_resp_cookie("_towerops_web_user_remember_me")
|> put_flash(:info, "Your session expired due to inactivity. Please log in again.")
|> put_flash(:info, t("Your session expired due to inactivity. Please log in again."))
|> redirect(to: "/users/log-in")
|> halt()
end