i18n: wrap all user-facing strings in gettext()
This commit is contained in:
parent
6623f2e24a
commit
243b773e39
92 changed files with 1200 additions and 1196 deletions
|
|
@ -43,6 +43,7 @@ defmodule ToweropsWeb do
|
|||
use Gettext, backend: ToweropsWeb.Gettext
|
||||
|
||||
import Plug.Conn
|
||||
import ToweropsWeb.GettextHelpers
|
||||
|
||||
unquote(verified_routes())
|
||||
end
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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} ->
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"])}
|
||||
|
|
|
|||
|
|
@ -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, "")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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, [])
|
||||
|
|
|
|||
|
|
@ -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 %>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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)}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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 %>
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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"}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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 %>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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} ->
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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: "—"
|
||||
|
|
|
|||
|
|
@ -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"}
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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)}
|
||||
|
||||
|
|
|
|||
|
|
@ -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} ->
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue