UI improvements

This commit is contained in:
Graham McIntire 2026-02-05 14:57:11 -06:00
parent 2ef85161dd
commit 156d9a47bf
No known key found for this signature in database
11 changed files with 82 additions and 37 deletions

View file

@ -820,16 +820,12 @@ defmodule ToweropsWeb.AgentChannel do
defp process_sensor_reading(sensor, oid_values, timestamp) do
case Map.get(oid_values, sensor.sensor_oid) do
nil ->
Logger.info("Sensor #{sensor.sensor_descr} (#{sensor.sensor_oid}): OID not in result")
:ok
value ->
with parsed_value when not is_nil(parsed_value) <- parse_float(value) do
final_value = parsed_value / sensor.sensor_divisor
Logger.info(
"Sensor #{sensor.sensor_descr}: raw=#{inspect(value)}, parsed=#{inspect(parsed_value)}, divisor=#{sensor.sensor_divisor}, final=#{inspect(final_value)}"
)
Snmp.create_sensor_reading(%{
sensor_id: sensor.id,
value: final_value,

View file

@ -205,7 +205,7 @@ defmodule ToweropsWeb.Layouts do
class="hidden md:ml-8 md:flex md:space-x-6"
>
<.nav_link
navigate={~p"/"}
navigate={~p"/dashboard"}
active={@active_page == "dashboard"}
>
Dashboard
@ -369,7 +369,7 @@ defmodule ToweropsWeb.Layouts do
>
<div class="space-y-1 px-2 pb-3 pt-2 border-t border-gray-200 dark:border-white/10">
<.mobile_nav_link
navigate={~p"/"}
navigate={~p"/dashboard"}
active={@active_page == "dashboard"}
>
<.icon name="hero-home" class="size-5" /> Dashboard

View file

@ -97,7 +97,7 @@
<.form for={@form} id="device-form" phx-change="validate" phx-submit="save">
<div class="divide-y divide-gray-200 dark:divide-white/10">
<!-- Basic Information -->
<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 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
@ -108,7 +108,7 @@
</div>
<div class="md:col-span-2">
<div class="grid grid-cols-1 gap-x-6 gap-y-8 sm:max-w-xl sm:grid-cols-6">
<div class="grid grid-cols-1 gap-x-6 gap-y-4 sm:max-w-xl sm:grid-cols-6">
<div class="col-span-full">
<.input
field={@form[:name]}
@ -186,7 +186,7 @@
</div>
<!-- Monitoring Configuration -->
<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 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
@ -197,7 +197,7 @@
</div>
<div class="md:col-span-2">
<div class="grid grid-cols-1 gap-x-6 gap-y-8 sm:max-w-xl sm:grid-cols-6">
<div class="grid grid-cols-1 gap-x-6 gap-y-4 sm:max-w-xl sm:grid-cols-6">
<div class="col-span-full">
<.input
field={@form[:check_interval_seconds]}
@ -276,7 +276,7 @@
</div>
<!-- SNMP Configuration -->
<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 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
@ -337,7 +337,7 @@
<!-- SNMP Fields -->
<div
:if={@monitoring_mode == "snmp_and_icmp"}
class="grid grid-cols-1 gap-x-6 gap-y-8 sm:max-w-xl sm:grid-cols-6"
class="grid grid-cols-1 gap-x-6 gap-y-4 sm:max-w-xl sm:grid-cols-6"
>
<div class="col-span-full">
<.input
@ -590,7 +590,7 @@
<%= if @live_action == :edit and @is_mikrotik_device do %>
<!-- MikroTik API Configuration -->
<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"
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"
phx-hook="MikrotikPortSync"
id="mikrotik-config-section"
>
@ -621,7 +621,7 @@
</div>
<% end %>
<div class="grid grid-cols-1 gap-x-6 gap-y-8 sm:max-w-xl sm:grid-cols-6">
<div class="grid grid-cols-1 gap-x-6 gap-y-4 sm:max-w-xl sm:grid-cols-6">
<div class="col-span-full">
<.input
field={@form[:mikrotik_enabled]}
@ -700,7 +700,7 @@
<!-- Danger Zone -->
<%= if @live_action == :edit do %>
<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 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>
<p class="mt-1 text-sm/6 text-gray-500 dark:text-gray-400">

View file

@ -26,7 +26,39 @@
<div class="flex items-center justify-between mb-4">
<div>
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">{@device.name}</h1>
<div class="flex items-center gap-3">
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">{@device.name}</h1>
<div class={[
"flex items-center gap-2 px-3 py-1.5 rounded-lg font-semibold text-sm transition-all",
case @device.status do
:up ->
"bg-green-100 text-green-800 dark:bg-green-950/50 dark:text-green-400 ring-1 ring-green-600/20 dark:ring-green-500/30"
:down ->
"bg-red-100 text-red-800 dark:bg-red-950/50 dark:text-red-400 ring-1 ring-red-600/20 dark:ring-red-500/30"
:unknown ->
"bg-gray-100 text-gray-800 dark:bg-gray-800/50 dark:text-gray-400 ring-1 ring-gray-600/20 dark:ring-gray-500/30"
end
]}>
<span class={[
"h-2 w-2 rounded-full animate-pulse",
case @device.status do
:up -> "bg-green-600 dark:bg-green-500"
:down -> "bg-red-600 dark:bg-red-500"
:unknown -> "bg-gray-500 dark:bg-gray-400"
end
]}>
</span>
<span>
{case @device.status do
:up -> "Online"
:down -> "Offline"
:unknown -> "Unknown"
end}
</span>
</div>
</div>
<div class="flex items-center gap-3 mt-0.5">
<span class="text-sm text-gray-600 dark:text-gray-400 font-mono">
{@device.ip_address}

View file

@ -5,7 +5,7 @@
<div class="border-b border-gray-200 pb-5 dark:border-white/5">
<div class="mb-4">
<.link
navigate={~p"/orgs/#{@organization.slug}"}
navigate={~p"/dashboard"}
class="inline-flex items-center gap-1 text-sm text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white"
>
<.icon name="hero-arrow-left" class="h-4 w-4" /> Back to Dashboard
@ -29,7 +29,7 @@
>
<div class="divide-y divide-gray-200 dark:divide-white/10">
<!-- Organization Name 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 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
@ -51,7 +51,7 @@
<%= if @user_orgs_count > 1 do %>
<!-- Default Organization 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 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
@ -91,7 +91,7 @@
<% end %>
<!-- Use Sites 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 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
@ -135,7 +135,7 @@
</div>
<!-- SNMP Configuration 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 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
@ -272,7 +272,7 @@
<%= if @current_scope.user.is_superuser do %>
<!-- MikroTik API Configuration 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 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
@ -363,7 +363,7 @@
<!-- Default Agent Section -->
<%= if @available_agents != [] do %>
<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 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
@ -441,7 +441,7 @@
</div>
</div>
<% else %>
<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 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
@ -468,7 +468,7 @@
<!-- Save Button at Bottom -->
<div class="flex items-center justify-end gap-x-6 border-t border-gray-200 px-4 py-4 sm:px-6 lg:px-8 dark:border-white/10">
<.link
navigate={~p"/orgs/#{@organization.slug}"}
navigate={~p"/dashboard"}
class="text-sm font-semibold text-gray-900 dark:text-white"
>
Cancel

View file

@ -2,6 +2,7 @@ defmodule ToweropsWeb.OrgLive.Index do
@moduledoc false
use ToweropsWeb, :live_view
alias Towerops.Accounts
alias Towerops.Organizations
@impl true
@ -19,4 +20,14 @@ defmodule ToweropsWeb.OrgLive.Index do
def handle_params(_params, _url, socket) do
{:noreply, socket}
end
@impl true
def handle_event("select_org", %{"org-id" => org_id}, socket) do
user = socket.assigns.current_scope.user
# Update user's default organization
{:ok, _user} = Accounts.update_user_profile(user, %{default_organization_id: org_id})
{:noreply, push_navigate(socket, to: ~p"/dashboard")}
end
end

View file

@ -12,10 +12,12 @@
</.header>
<div :if={@organizations != []} class="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
<.link
<button
:for={org <- @organizations}
navigate={~p"/orgs/#{org.slug}"}
class="relative overflow-hidden rounded-lg border border-gray-200 bg-white p-6 shadow-sm hover:shadow-md transition-shadow dark:border-white/10 dark:bg-gray-800/50 cursor-pointer"
type="button"
phx-click="select_org"
phx-value-org-id={org.id}
class="relative overflow-hidden rounded-lg border border-gray-200 bg-white p-6 shadow-sm hover:shadow-md transition-shadow dark:border-white/10 dark:bg-gray-800/50 cursor-pointer text-left"
>
<h2 class="text-xl font-semibold text-gray-900 dark:text-white">{org.name}</h2>
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">
@ -23,7 +25,7 @@
|> to_string()
|> String.capitalize()}
</p>
</.link>
</button>
</div>
<div :if={@organizations == []} class="text-center py-16">

View file

@ -39,11 +39,11 @@ defmodule ToweropsWeb.OrgLive.New do
opts = if is_superuser, do: [bypass_limits: true], else: []
case Organizations.create_organization(org_params, user.id, opts) do
{:ok, organization} ->
{:ok, _organization} ->
{:noreply,
socket
|> put_flash(:info, "Organization created successfully")
|> push_navigate(to: ~p"/orgs/#{organization.slug}")}
|> push_navigate(to: ~p"/dashboard")}
{:error, %Ecto.Changeset{} = changeset} ->
{:noreply, assign(socket, :form, to_form(changeset))}

View file

@ -293,7 +293,6 @@ defmodule ToweropsWeb.Router do
scope "/orgs/:org_slug", ToweropsWeb do
pipe_through [:browser, :require_authenticated_user, :load_current_organization]
live "/", DashboardLive, :index
live "/settings", Org.SettingsLive, :index
end
end
@ -309,6 +308,9 @@ defmodule ToweropsWeb.Router do
scope "/", ToweropsWeb do
pipe_through [:browser, :require_authenticated_user]
# Dashboard route
live "/dashboard", DashboardLive, :index
# Alert routes
live "/alerts", AlertLive.Index, :index

View file

@ -323,8 +323,8 @@ defmodule ToweropsWeb.UserAuth do
# Get user's organizations (ordered by most recently joined first)
case Towerops.Organizations.list_user_organizations(user.id) do
[_first_org | _] ->
# Devices page
~p"/devices"
# Dashboard page
~p"/dashboard"
[] ->
# No organizations yet, go to org list
@ -883,10 +883,10 @@ defmodule ToweropsWeb.UserAuth do
ip_address: ip
})
# Devices page
# Dashboard page
redirect_path =
case Towerops.Organizations.list_user_organizations(superuser.id) do
[_first_org | _] -> ~p"/devices"
[_first_org | _] -> ~p"/dashboard"
[] -> ~p"/orgs"
end

View file

@ -5,6 +5,8 @@ Devices Tested & Working
2026-02-05
* Fix SNMP v3 polling
* UI Simplification
* Add device status to device page
2026-02-04
* Feature: SNMP v3 support