"" end)
+
+ ~H"""
+
+ """
+ end
+
@doc """
- A pulsing card placeholder.
+ A pulsing card placeholder matching stat cards.
"""
attr :class, :string, default: nil
@@ -15,9 +24,9 @@ defmodule ToweropsWeb.Components.Skeletons do
"animate-pulse rounded-lg border border-gray-200 bg-white p-4 shadow-sm dark:border-white/10 dark:bg-gray-800/50",
@class
]}>
-
-
-
+ <.skel class="h-3 w-24" />
+ <.skel class="mt-3 h-8 w-16" />
+ <.skel class="mt-2 h-3 w-20" />
"""
end
@@ -30,18 +39,33 @@ defmodule ToweropsWeb.Components.Skeletons do
def skeleton_table(assigns) do
~H"""
-
-
-
-
+
+
+
+
@@ -59,47 +83,72 @@ defmodule ToweropsWeb.Components.Skeletons do
"animate-pulse rounded-lg border border-gray-200 bg-white p-4 shadow-sm dark:border-white/10 dark:bg-gray-800/50",
@class
]}>
-
-
+
+
+
"""
end
@doc """
- Dashboard loading skeleton - full placeholder for the dashboard content.
+ Dashboard loading skeleton — mirrors the real dashboard layout.
"""
def skeleton_dashboard(assigns) do
~H"""
+
<.skeleton_stat :for={_ <- 1..6} />
+
-
+
+
+
diff --git a/lib/towerops_web/controllers/user_registration_html/new.html.heex b/lib/towerops_web/controllers/user_registration_html/new.html.heex
index cfe3cd02..d3cada4f 100644
--- a/lib/towerops_web/controllers/user_registration_html/new.html.heex
+++ b/lib/towerops_web/controllers/user_registration_html/new.html.heex
@@ -1,139 +1,212 @@
-
-
- <.header>
- <%= if @invitation do %>
- {t_auth("Join %{organization}", organization: @invitation.organization.name)}
- <% else %>
- {t_auth("Register for an account")}
- <% end %>
- <:subtitle>
- {t_auth("Already registered?")}
- <.link
- navigate={~p"/users/log-in"}
- class="font-semibold text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"
- >
- {t_auth("Log in")}
-
- {t_auth("to your account now.")}
-
-
-
- <%= if @invitation do %>
-
-
- {t_auth("You've been invited to join %{organization} as a %{role}.",
- organization: @invitation.organization.name,
- role: @invitation.role
- )}
+
+
+
+
+
+
+ <.icon name="hero-signal" class="size-8" />
+ TowerOps
+
+
+ {t("Start monitoring your network in minutes.")}
- <% else %>
-
-
- {t_auth("Free tier includes:")}
-
-
- - {t_auth("✓ Monitor up to 10 devices")}
- - {t_auth("✓ Real-time alerts and notifications")}
- - {t_auth("✓ Performance charts and historical data")}
- - {t_auth("✓ No credit card required")}
-
+
+
+ {t("Free tier includes:")}
+
+ -
+ <.icon name="hero-check" class="size-4 text-green-300" />
+ {t("Monitor up to 10 devices")}
+
+ -
+ <.icon name="hero-check" class="size-4 text-green-300" />
+ {t("Real-time alerts")}
+
+ -
+ <.icon name="hero-check" class="size-4 text-green-300" />
+ {t("Performance charts & history")}
+
+ -
+ <.icon name="hero-check" class="size-4 text-green-300" />
+ {t("No credit card required")}
+
+
+
+
+
+
+
+
+
+
+ <.icon name="hero-signal" class="size-7 text-blue-600 dark:text-blue-400" />
+ TowerOps
- <% end %>
-
-
-
- <.form :let={f} for={@form} action={~p"/users/register"}>
- <%= if @invitation_token do %>
-
- <% end %>
-
- <.input
- field={f[:email]}
- type="email"
- label={t_auth("Email")}
- autocomplete="email"
- required
- phx-mounted={JS.focus()}
- />
- <.input
- field={f[:password]}
- type="password"
- label={t_auth("Password")}
- autocomplete="new-password"
- required
- />
+
+
+ <%= if @invitation do %>
+ {t_auth("Join %{organization}", organization: @invitation.organization.name)}
+ <% else %>
+ {t_auth("Create your account")}
+ <% end %>
+
+
+ {t_auth("Already registered?")}
+ <.link
+ navigate={~p"/users/log-in"}
+ class="font-semibold text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"
+ >
+ {t_auth("Log in")}
+
+
+
+
+
<%= unless @invitation do %>
- <.input
- name="user[organization_name]"
- value=""
- type="text"
- label={t_auth("Organization Name")}
- placeholder={t_auth("My Company")}
- required
- />
+
+ <.icon name="hero-gift" class="size-5 flex-shrink-0" />
+
+ {t_auth("First 10 devices free — no credit card needed")}
+
+
<% end %>
-
-
-
-
-
-
-
-
+ <%= if @invitation do %>
+
+
+ {t_auth("You've been invited to join %{organization} as a %{role}.",
+ organization: @invitation.organization.name,
+ role: @invitation.role
+ )}
+
+ <% end %>
-
-
-
+ <.form :let={f} for={@form} action={~p"/users/register"}>
+ <%= if @invitation_token do %>
+
+ <% end %>
+
+ <.input
+ field={f[:email]}
+ type="email"
+ label={t_auth("Email")}
+ autocomplete="email"
+ required
+ phx-mounted={JS.focus()}
+ />
+ <.input
+ field={f[:password]}
+ type="password"
+ label={t_auth("Password")}
+ autocomplete="new-password"
+ required
+ />
+
+
+
+
+ {t_auth("Use 12+ characters for a strong password")}
+
+
+
+ <%= unless @invitation do %>
+ <.input
+ name="user[organization_name]"
+ value=""
+ type="text"
+ label={t_auth("Organization Name")}
+ placeholder={t_auth("My Company")}
required
- class="h-4 w-4 rounded border-zinc-300 text-blue-600 focus:ring-blue-500 dark:border-zinc-600 dark:bg-zinc-800 dark:checked:bg-blue-500 dark:checked:border-blue-500"
/>
+ <% end %>
+
+
-
-
-
-
+
+ <.button
+ phx-disable-with={t_auth("Creating account...")}
+ class="w-full mt-6"
+ variant="primary"
+ >
+ {if @invitation,
+ do: t_auth("Accept invitation and create account"),
+ else: t_auth("Create an account")}
+ →
+
+
-
- <.button
- phx-disable-with={t_auth("Creating account...")}
- class="w-full mt-6"
- variant="primary"
- >
- {if @invitation,
- do: t_auth("Accept invitation and create account"),
- else: t_auth("Create an account")}
-
-
+
diff --git a/lib/towerops_web/controllers/user_session_html/new.html.heex b/lib/towerops_web/controllers/user_session_html/new.html.heex
index 41463891..c6cd6339 100644
--- a/lib/towerops_web/controllers/user_session_html/new.html.heex
+++ b/lib/towerops_web/controllers/user_session_html/new.html.heex
@@ -1,119 +1,168 @@
-
-
- <.header>
- {t_auth("Log in")}
- <:subtitle>
- <%= if @current_scope do %>
- {t_auth("You need to reauthenticate to perform sensitive actions on your account.")}
- <% else %>
- {t_auth("Don't have an account?")}
- <.link
- navigate={~p"/users/register"}
- class="font-semibold text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"
- >
- {t_auth("Sign up")}
-
- {t_auth("for an account now.")}
- <% end %>
-
-
-
-
-
-
- <.icon
- name="hero-information-circle"
- class="h-6 w-6 flex-shrink-0 text-blue-600 dark:text-blue-400"
- />
-
-
- {t_auth("You are running the local mail adapter.")}
-
-
- {t_auth("To see sent emails, visit")}
-
- {t_auth("the mailbox page")}
- .
+
+
+
+
+
+
+ <.icon name="hero-signal" class="size-8" />
+ TowerOps
+
+
+ {t("Monitor your entire network from a single dashboard.")}
-
-
-
-
-
- <.form :let={f} for={@form} as={:user} id="login_form_password" action={~p"/users/log-in"}>
- <.input
- readonly={!!@current_scope}
- field={f[:email]}
- id="user_email_password"
- type="email"
- label={t_auth("Email")}
- autocomplete="email"
- required
- phx-mounted={JS.focus()}
- />
- <.input
- field={f[:password]}
- id="user_password"
- type="password"
- label={t_auth("Password")}
- autocomplete="current-password"
- required
- />
-
- <.link
- navigate={~p"/users/reset-password"}
- class="text-sm font-semibold text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"
- >
- {t_auth("Forgot password?")}
-
-
-
- <.button class="w-full" variant="primary" name={@form[:remember_me].name} value="true">
- {t_auth("Log in")} →
-
-
-
-
-
-
-
-
- {t("or")}
-
+
+
+
+ {t(
+ "\"TowerOps cut our response time to network issues by 80%. We can see problems before customers even notice.\""
+ )}
+
+ {t("— WISP Network Operator")}
+
-
- <.form :let={f} for={@form} as={:user} id="login_form_magic" action={~p"/users/log-in"}>
- <.input
- readonly={!!@current_scope}
- field={f[:email]}
- id="user_email_magic"
- type="email"
- label={t_auth("Email")}
- autocomplete="email"
- required
- />
- <.button class="w-full">
- {t_auth("Send me a login link instead")}
-
-
+
+
+
+
+ <.icon name="hero-signal" class="size-7 text-blue-600 dark:text-blue-400" />
+ TowerOps
+
-
- <.link
- href={~p"/users/confirm"}
- class="text-blue-600 hover:text-blue-700 dark:text-blue-400"
+
+
+ {t_auth("Log in")}
+
+
+ <%= if @current_scope do %>
+ {t_auth("You need to reauthenticate to perform sensitive actions on your account.")}
+ <% else %>
+ {t_auth("Don't have an account?")}
+ <.link
+ navigate={~p"/users/register"}
+ class="font-semibold text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"
+ >
+ {t_auth("Sign up")}
+
+ <% end %>
+
+
+
+
- {t_auth("Didn't receive confirmation instructions?")}
-
-
+
+ <.icon
+ name="hero-information-circle"
+ class="h-6 w-6 flex-shrink-0 text-blue-600 dark:text-blue-400"
+ />
+
+
+
+
+
+
+ <.form
+ :let={f}
+ for={@form}
+ as={:user}
+ id="login_form_password"
+ action={~p"/users/log-in"}
+ >
+ <.input
+ readonly={!!@current_scope}
+ field={f[:email]}
+ id="user_email_password"
+ type="email"
+ label={t_auth("Email")}
+ autocomplete="email"
+ required
+ phx-mounted={JS.focus()}
+ />
+ <.input
+ field={f[:password]}
+ id="user_password"
+ type="password"
+ label={t_auth("Password")}
+ autocomplete="current-password"
+ required
+ />
+
+
+ <.link
+ navigate={~p"/users/reset-password"}
+ class="text-sm font-semibold text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"
+ >
+ {t_auth("Forgot password?")}
+
+
+
+ <.button class="w-full" variant="primary">
+ {t_auth("Log in")} →
+
+
+
+
+
+
+
+ <.form :let={f} for={@form} as={:user} id="login_form_magic" action={~p"/users/log-in"}>
+ <.input
+ readonly={!!@current_scope}
+ field={f[:email]}
+ id="user_email_magic"
+ type="email"
+ label={t_auth("Email")}
+ autocomplete="email"
+ required
+ />
+ <.button class="w-full" variant="secondary">
+ <.icon name="hero-envelope" class="size-4" />
+ {t_auth("Send me a login link instead")}
+
+
+
+
+ <.link
+ href={~p"/users/confirm"}
+ class="text-blue-600 hover:text-blue-700 dark:text-blue-400 hover:underline"
+ >
+ {t_auth("Didn't receive confirmation instructions?")}
+
+
+
+
diff --git a/lib/towerops_web/live/device_live/show.html.heex b/lib/towerops_web/live/device_live/show.html.heex
index 6943ad05..4bd50064 100644
--- a/lib/towerops_web/live/device_live/show.html.heex
+++ b/lib/towerops_web/live/device_live/show.html.heex
@@ -17,7 +17,7 @@
} />
-
+
{@device.name}
-
@@ -599,8 +607,16 @@
data-chart={@latency_chart_data}
data-unit="ms"
data-auto-scale="true"
+ class="relative"
style="height: 300px;"
>
+
+
+
+
+ {t("Loading chart...")}
+
+
@@ -1243,54 +1259,64 @@
- | # |
- Name |
- Status |
- Speed |
- IP Address |
- MAC |
+ # |
+ Name |
+ Status |
+ Speed |
+ IP Address |
+ MAC |
+ In |
+ Out |
<%= for interface <- interfaces do %>
-
- |
+ |
+ |
{interface.if_index}
|
-
+ |
<.link
navigate={
~p"/devices/#{@device.id}/graph/traffic?interface_id=#{interface.id}"
}
class="hover:text-blue-600 dark:hover:text-blue-400"
>
-
+
{interface.if_name || interface.if_descr}
<%= if interface.if_alias do %>
-
+
{interface.if_alias}
<% end %>
|
-
-
- {String.upcase(interface.if_oper_status || "unknown")}
+ |
+
+
+
+ {String.upcase(interface.if_oper_status || "unknown")}
+
|
-
+ |
{format_speed(interface.if_speed)}
|
-
+ |
<% interface_ips =
Map.get(@ip_addresses_by_interface, interface.id, []) %>
<%= if Enum.empty?(interface_ips) do %>
@@ -1298,18 +1324,54 @@
<% else %>
<%= for ip <- interface_ips do %>
-
+
{ip.ip_address}
<%= if ip.prefix_length do %>
/{ip.prefix_length}
<% end %>
+ <.icon
+ name="hero-clipboard-document"
+ class="h-3 w-3 inline ml-0.5 opacity-0 group-hover/iip:opacity-100 transition-opacity text-gray-400"
+ />
<% end %>
<% end %>
|
-
- {interface.if_phys_address || "-"}
+ |
+
+ {interface.if_phys_address}
+ <.icon
+ name="hero-clipboard-document"
+ class="h-3 w-3 inline ml-0.5 opacity-0 group-hover/mac:opacity-100 transition-opacity text-gray-400"
+ />
+
+ -
+ |
+
+ {if interface.if_in_octets,
+ do: format_bytes(interface.if_in_octets),
+ else: "-"}
+ |
+
+ {if interface.if_out_octets,
+ do: format_bytes(interface.if_out_octets),
+ else: "-"}
|
<% end %>
|