help update
This commit is contained in:
parent
6f6627a54d
commit
aa0e7f3d05
6 changed files with 820 additions and 56 deletions
109
lib/towerops/contexts/config_change_tracker.ex
Normal file
109
lib/towerops/contexts/config_change_tracker.ex
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
defmodule Towerops.Contexts.ConfigChangeTracker do
|
||||
@moduledoc """
|
||||
Tracks configuration changes for SNMP and MikroTik settings across
|
||||
organizations and sites to trigger device propagation.
|
||||
|
||||
Provides functions to capture configuration state before updates and
|
||||
detect which configuration fields changed after updates.
|
||||
"""
|
||||
|
||||
@config_fields [
|
||||
:snmp_community,
|
||||
:mikrotik_username,
|
||||
:mikrotik_password,
|
||||
:mikrotik_port,
|
||||
:mikrotik_use_ssl,
|
||||
:mikrotik_enabled
|
||||
]
|
||||
|
||||
@doc """
|
||||
Captures the current configuration state from a struct.
|
||||
|
||||
Returns a map with all tracked configuration fields.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> org = %Organization{snmp_community: "public", mikrotik_username: "admin"}
|
||||
iex> ConfigChangeTracker.capture_config_state(org)
|
||||
%{
|
||||
snmp_community: "public",
|
||||
mikrotik_username: "admin",
|
||||
mikrotik_password: nil,
|
||||
mikrotik_port: nil,
|
||||
mikrotik_use_ssl: nil,
|
||||
mikrotik_enabled: nil
|
||||
}
|
||||
"""
|
||||
@spec capture_config_state(struct()) :: map()
|
||||
def capture_config_state(struct) do
|
||||
Map.new(@config_fields, fn field ->
|
||||
{field, Map.get(struct, field)}
|
||||
end)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Checks if the SNMP community string changed.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> old_state = %{snmp_community: "public"}
|
||||
iex> updated = %Organization{snmp_community: "private"}
|
||||
iex> ConfigChangeTracker.snmp_changed?(updated, old_state)
|
||||
true
|
||||
"""
|
||||
@spec snmp_changed?(struct(), map()) :: boolean()
|
||||
def snmp_changed?(updated_struct, old_state) do
|
||||
Map.get(updated_struct, :snmp_community) != old_state.snmp_community
|
||||
end
|
||||
|
||||
@doc """
|
||||
Checks if any MikroTik configuration field changed.
|
||||
|
||||
Compares all MikroTik-related fields (username, password, port, ssl, enabled)
|
||||
except snmp_community.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> old_state = %{mikrotik_username: "admin", mikrotik_password: "pass"}
|
||||
iex> updated = %Organization{mikrotik_username: "root", mikrotik_password: "pass"}
|
||||
iex> ConfigChangeTracker.mikrotik_changed?(updated, old_state)
|
||||
true
|
||||
"""
|
||||
@spec mikrotik_changed?(struct(), map()) :: boolean()
|
||||
def mikrotik_changed?(updated_struct, old_state) do
|
||||
Enum.any?(@config_fields, fn field ->
|
||||
case field do
|
||||
:snmp_community -> false
|
||||
field -> Map.get(updated_struct, field) != old_state[field]
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Extracts MikroTik configuration attributes from a struct.
|
||||
|
||||
Returns a map with all MikroTik-related fields suitable for device propagation.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> site = %Site{mikrotik_username: "admin", mikrotik_port: 8729}
|
||||
iex> ConfigChangeTracker.extract_mikrotik_attrs(site)
|
||||
%{
|
||||
mikrotik_username: "admin",
|
||||
mikrotik_password: nil,
|
||||
mikrotik_port: 8729,
|
||||
mikrotik_use_ssl: nil,
|
||||
mikrotik_enabled: nil
|
||||
}
|
||||
"""
|
||||
@spec extract_mikrotik_attrs(struct()) :: map()
|
||||
def extract_mikrotik_attrs(struct) do
|
||||
%{
|
||||
mikrotik_username: Map.get(struct, :mikrotik_username),
|
||||
mikrotik_password: Map.get(struct, :mikrotik_password),
|
||||
mikrotik_port: Map.get(struct, :mikrotik_port),
|
||||
mikrotik_use_ssl: Map.get(struct, :mikrotik_use_ssl),
|
||||
mikrotik_enabled: Map.get(struct, :mikrotik_enabled)
|
||||
}
|
||||
end
|
||||
end
|
||||
|
|
@ -6,6 +6,7 @@ defmodule Towerops.Organizations do
|
|||
import Ecto.Query
|
||||
|
||||
alias Towerops.Agents.AgentAssignment
|
||||
alias Towerops.Contexts.ConfigChangeTracker
|
||||
alias Towerops.Devices.Device
|
||||
alias Towerops.Organizations.Invitation
|
||||
alias Towerops.Organizations.Membership
|
||||
|
|
@ -124,30 +125,23 @@ defmodule Towerops.Organizations do
|
|||
Updates an organization.
|
||||
"""
|
||||
def update_organization(%Organization{} = organization, attrs) do
|
||||
old_values = %{
|
||||
snmp_community: organization.snmp_community,
|
||||
mikrotik_username: organization.mikrotik_username,
|
||||
mikrotik_password: organization.mikrotik_password,
|
||||
mikrotik_port: organization.mikrotik_port,
|
||||
mikrotik_use_ssl: organization.mikrotik_use_ssl,
|
||||
mikrotik_enabled: organization.mikrotik_enabled,
|
||||
use_sites: organization.use_sites
|
||||
}
|
||||
old_config = ConfigChangeTracker.capture_config_state(organization)
|
||||
old_use_sites = organization.use_sites
|
||||
|
||||
with {:ok, updated_organization} <- organization |> Organization.changeset(attrs) |> Repo.update() do
|
||||
propagate_organization_changes(updated_organization, old_values)
|
||||
propagate_organization_changes(updated_organization, old_config, old_use_sites)
|
||||
{:ok, updated_organization}
|
||||
end
|
||||
end
|
||||
|
||||
defp propagate_organization_changes(updated_organization, old_values) do
|
||||
propagate_snmp_changes(updated_organization, old_values)
|
||||
propagate_mikrotik_changes(updated_organization, old_values)
|
||||
handle_sites_disabled(updated_organization, old_values)
|
||||
defp propagate_organization_changes(updated_organization, old_config, old_use_sites) do
|
||||
propagate_snmp_changes(updated_organization, old_config)
|
||||
propagate_mikrotik_changes(updated_organization, old_config)
|
||||
handle_sites_disabled(updated_organization, old_use_sites)
|
||||
end
|
||||
|
||||
defp propagate_snmp_changes(updated_organization, %{snmp_community: old_community}) do
|
||||
if updated_organization.snmp_community != old_community do
|
||||
defp propagate_snmp_changes(updated_organization, old_config) do
|
||||
if ConfigChangeTracker.snmp_changed?(updated_organization, old_config) do
|
||||
Towerops.Devices.propagate_organization_community_change(
|
||||
updated_organization.id,
|
||||
updated_organization.snmp_community
|
||||
|
|
@ -155,22 +149,9 @@ defmodule Towerops.Organizations do
|
|||
end
|
||||
end
|
||||
|
||||
defp propagate_mikrotik_changes(updated_organization, old_values) do
|
||||
mikrotik_changed =
|
||||
updated_organization.mikrotik_username != old_values.mikrotik_username ||
|
||||
updated_organization.mikrotik_password != old_values.mikrotik_password ||
|
||||
updated_organization.mikrotik_port != old_values.mikrotik_port ||
|
||||
updated_organization.mikrotik_use_ssl != old_values.mikrotik_use_ssl ||
|
||||
updated_organization.mikrotik_enabled != old_values.mikrotik_enabled
|
||||
|
||||
if mikrotik_changed do
|
||||
mikrotik_attrs = %{
|
||||
mikrotik_username: updated_organization.mikrotik_username,
|
||||
mikrotik_password: updated_organization.mikrotik_password,
|
||||
mikrotik_port: updated_organization.mikrotik_port,
|
||||
mikrotik_use_ssl: updated_organization.mikrotik_use_ssl,
|
||||
mikrotik_enabled: updated_organization.mikrotik_enabled
|
||||
}
|
||||
defp propagate_mikrotik_changes(updated_organization, old_config) do
|
||||
if ConfigChangeTracker.mikrotik_changed?(updated_organization, old_config) do
|
||||
mikrotik_attrs = ConfigChangeTracker.extract_mikrotik_attrs(updated_organization)
|
||||
|
||||
Towerops.Devices.propagate_organization_mikrotik_change(
|
||||
updated_organization.id,
|
||||
|
|
@ -179,7 +160,7 @@ defmodule Towerops.Organizations do
|
|||
end
|
||||
end
|
||||
|
||||
defp handle_sites_disabled(updated_organization, %{use_sites: old_use_sites}) do
|
||||
defp handle_sites_disabled(updated_organization, old_use_sites) do
|
||||
if old_use_sites && !updated_organization.use_sites do
|
||||
clear_all_site_assignments(updated_organization.id)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ defmodule Towerops.Sites do
|
|||
import Ecto.Query
|
||||
|
||||
alias Towerops.Agents.AgentAssignment
|
||||
alias Towerops.Contexts.ConfigChangeTracker
|
||||
alias Towerops.Devices.Device
|
||||
alias Towerops.Repo
|
||||
alias Towerops.Sites.Site
|
||||
|
|
@ -106,39 +107,20 @@ defmodule Towerops.Sites do
|
|||
Updates a site.
|
||||
"""
|
||||
def update_site(%Site{} = site, attrs) do
|
||||
old_community = site.snmp_community
|
||||
old_mikrotik_username = site.mikrotik_username
|
||||
old_mikrotik_password = site.mikrotik_password
|
||||
old_mikrotik_port = site.mikrotik_port
|
||||
old_mikrotik_use_ssl = site.mikrotik_use_ssl
|
||||
old_mikrotik_enabled = site.mikrotik_enabled
|
||||
old_config = ConfigChangeTracker.capture_config_state(site)
|
||||
|
||||
case site
|
||||
|> Site.changeset(attrs)
|
||||
|> Repo.update() do
|
||||
{:ok, updated_site} = result ->
|
||||
# If community string changed, propagate to inheriting devices
|
||||
if updated_site.snmp_community != old_community do
|
||||
if ConfigChangeTracker.snmp_changed?(updated_site, old_config) do
|
||||
Towerops.Devices.propagate_site_community_change(updated_site.id, updated_site.snmp_community)
|
||||
end
|
||||
|
||||
# If any MikroTik settings changed, propagate to inheriting devices
|
||||
mikrotik_changed =
|
||||
updated_site.mikrotik_username != old_mikrotik_username ||
|
||||
updated_site.mikrotik_password != old_mikrotik_password ||
|
||||
updated_site.mikrotik_port != old_mikrotik_port ||
|
||||
updated_site.mikrotik_use_ssl != old_mikrotik_use_ssl ||
|
||||
updated_site.mikrotik_enabled != old_mikrotik_enabled
|
||||
|
||||
if mikrotik_changed do
|
||||
mikrotik_attrs = %{
|
||||
mikrotik_username: updated_site.mikrotik_username,
|
||||
mikrotik_password: updated_site.mikrotik_password,
|
||||
mikrotik_port: updated_site.mikrotik_port,
|
||||
mikrotik_use_ssl: updated_site.mikrotik_use_ssl,
|
||||
mikrotik_enabled: updated_site.mikrotik_enabled
|
||||
}
|
||||
|
||||
if ConfigChangeTracker.mikrotik_changed?(updated_site, old_config) do
|
||||
mikrotik_attrs = ConfigChangeTracker.extract_mikrotik_attrs(updated_site)
|
||||
Towerops.Devices.propagate_site_mikrotik_change(updated_site.id, mikrotik_attrs)
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -76,6 +76,15 @@
|
|||
Account Data (GDPR)
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="#graphs"
|
||||
data-nav-link="graphs"
|
||||
class="nav-link block py-1 pl-4 pr-3 text-sm text-gray-600 transition hover:text-gray-900 dark:text-gray-400 dark:hover:text-white"
|
||||
>
|
||||
Graphs & Live Polling
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
|
|
|
|||
|
|
@ -238,6 +238,21 @@ defmodule ToweropsWeb.HelpLive.Index do
|
|||
MikroTik
|
||||
</.link>
|
||||
</li>
|
||||
<li>
|
||||
<.link
|
||||
patch={~p"/help?section=graphs"}
|
||||
class={[
|
||||
"block px-3 py-2 rounded-md text-sm font-medium transition-colors",
|
||||
if @active_section == "graphs" do
|
||||
"bg-blue-50 text-blue-700 dark:bg-blue-900/50 dark:text-blue-300"
|
||||
else
|
||||
"text-gray-700 hover:bg-gray-50 dark:text-gray-300 dark:hover:bg-gray-800"
|
||||
end
|
||||
]}
|
||||
>
|
||||
Graphs & Live Polling
|
||||
</.link>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
|
|
@ -951,6 +966,361 @@ defmodule ToweropsWeb.HelpLive.Index do
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% "graphs" -> %>
|
||||
<div class="p-6">
|
||||
<h2 class="text-xl font-bold text-gray-900 dark:text-white mb-4">
|
||||
Graphs & Live Polling
|
||||
</h2>
|
||||
|
||||
<div class="prose prose-sm dark:prose-invert max-w-none">
|
||||
<p class="text-gray-600 dark:text-gray-400">
|
||||
Towerops provides powerful visualization capabilities for all monitored metrics. Every sensor,
|
||||
interface, and metric can be viewed as a time-series graph with multiple time ranges and a
|
||||
real-time live polling mode for instant feedback.
|
||||
</p>
|
||||
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mt-6 mb-3">
|
||||
Accessing Graphs
|
||||
</h3>
|
||||
|
||||
<p class="text-gray-600 dark:text-gray-400">
|
||||
Graphs are available throughout Towerops wherever metrics are displayed:
|
||||
</p>
|
||||
|
||||
<ul class="space-y-2 text-gray-600 dark:text-gray-400 list-disc list-inside mt-3">
|
||||
<li>
|
||||
<strong class="text-gray-900 dark:text-white">Device Overview:</strong>
|
||||
Click any metric tile (CPU, Memory, Temperature, etc.) to view its graph
|
||||
</li>
|
||||
<li>
|
||||
<strong class="text-gray-900 dark:text-white">Sensors Tab:</strong>
|
||||
Click the graph icon next to any sensor reading
|
||||
</li>
|
||||
<li>
|
||||
<strong class="text-gray-900 dark:text-white">Interfaces Tab:</strong>
|
||||
Click the graph icon next to any interface to view traffic graphs
|
||||
</li>
|
||||
<li>
|
||||
<strong class="text-gray-900 dark:text-white">Storage Tab:</strong>
|
||||
Click any storage volume to view usage over time
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mt-8 mb-3">
|
||||
Time Ranges
|
||||
</h3>
|
||||
|
||||
<p class="text-gray-600 dark:text-gray-400">
|
||||
All graphs support multiple time ranges for analyzing trends at different scales:
|
||||
</p>
|
||||
|
||||
<div class="mt-4 grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
<div class="p-3 bg-gray-50 dark:bg-gray-800/50 border border-gray-200 dark:border-white/10 rounded-lg">
|
||||
<h4 class="text-sm font-semibold text-gray-900 dark:text-white mb-1">
|
||||
1 Hour
|
||||
</h4>
|
||||
<p class="text-xs text-gray-600 dark:text-gray-400">
|
||||
Recent activity with high detail
|
||||
</p>
|
||||
</div>
|
||||
<div class="p-3 bg-gray-50 dark:bg-gray-800/50 border border-gray-200 dark:border-white/10 rounded-lg">
|
||||
<h4 class="text-sm font-semibold text-gray-900 dark:text-white mb-1">
|
||||
6 Hours
|
||||
</h4>
|
||||
<p class="text-xs text-gray-600 dark:text-gray-400">
|
||||
Half-day trends and patterns
|
||||
</p>
|
||||
</div>
|
||||
<div class="p-3 bg-gray-50 dark:bg-gray-800/50 border border-gray-200 dark:border-white/10 rounded-lg">
|
||||
<h4 class="text-sm font-semibold text-gray-900 dark:text-white mb-1">
|
||||
12 Hours
|
||||
</h4>
|
||||
<p class="text-xs text-gray-600 dark:text-gray-400">
|
||||
Business day overview
|
||||
</p>
|
||||
</div>
|
||||
<div class="p-3 bg-gray-50 dark:bg-gray-800/50 border border-gray-200 dark:border-white/10 rounded-lg">
|
||||
<h4 class="text-sm font-semibold text-gray-900 dark:text-white mb-1">
|
||||
24 Hours (Default)
|
||||
</h4>
|
||||
<p class="text-xs text-gray-600 dark:text-gray-400">
|
||||
Full day of activity
|
||||
</p>
|
||||
</div>
|
||||
<div class="p-3 bg-gray-50 dark:bg-gray-800/50 border border-gray-200 dark:border-white/10 rounded-lg">
|
||||
<h4 class="text-sm font-semibold text-gray-900 dark:text-white mb-1">
|
||||
7 Days
|
||||
</h4>
|
||||
<p class="text-xs text-gray-600 dark:text-gray-400">
|
||||
Weekly trends and patterns
|
||||
</p>
|
||||
</div>
|
||||
<div class="p-3 bg-gray-50 dark:bg-gray-800/50 border border-gray-200 dark:border-white/10 rounded-lg">
|
||||
<h4 class="text-sm font-semibold text-gray-900 dark:text-white mb-1">
|
||||
30 Days
|
||||
</h4>
|
||||
<p class="text-xs text-gray-600 dark:text-gray-400">
|
||||
Monthly overview and capacity planning
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mt-8 mb-3">
|
||||
Live Polling Mode
|
||||
</h3>
|
||||
|
||||
<p class="text-gray-600 dark:text-gray-400">
|
||||
Live mode provides real-time sensor monitoring with data updating every second. This is perfect for:
|
||||
</p>
|
||||
|
||||
<ul class="space-y-2 text-gray-600 dark:text-gray-400 list-disc list-inside mt-3">
|
||||
<li>Testing configuration changes and seeing immediate effects</li>
|
||||
<li>Monitoring system load during maintenance or upgrades</li>
|
||||
<li>Watching temperature changes during thermal testing</li>
|
||||
<li>Observing traffic patterns during load testing</li>
|
||||
<li>Real-time troubleshooting of performance issues</li>
|
||||
</ul>
|
||||
|
||||
<div class="mt-6 p-4 bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-lg">
|
||||
<div class="flex gap-3">
|
||||
<.icon
|
||||
name="hero-signal"
|
||||
class="h-5 w-5 text-green-600 dark:text-green-400 flex-shrink-0 mt-0.5"
|
||||
/>
|
||||
<div>
|
||||
<h4 class="text-sm font-semibold text-green-900 dark:text-green-300 mb-1">
|
||||
How Live Mode Works
|
||||
</h4>
|
||||
<ul class="text-sm text-green-800 dark:text-green-300 space-y-1 list-disc list-inside">
|
||||
<li>Polls sensors directly via SNMP every 1 second</li>
|
||||
<li>Displays a rolling 5-minute window (300 data points)</li>
|
||||
<li>Updates chart in real-time as new data arrives</li>
|
||||
<li>Automatically stops when you switch to another time range</li>
|
||||
<li>Works with remote pollers - polling happens on the agent</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mt-8 mb-3">
|
||||
Using Live Mode
|
||||
</h3>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="flex gap-4">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="flex items-center justify-center w-8 h-8 rounded-full bg-blue-100 dark:bg-blue-900/50 text-blue-700 dark:text-blue-300 font-semibold">
|
||||
1
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<h4 class="text-base font-semibold text-gray-900 dark:text-white mb-2">
|
||||
Open Any Graph
|
||||
</h4>
|
||||
<p class="text-gray-600 dark:text-gray-400">
|
||||
Navigate to any device and click on a metric graph (CPU, Memory, Temperature, Traffic, etc.)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-4">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="flex items-center justify-center w-8 h-8 rounded-full bg-blue-100 dark:bg-blue-900/50 text-blue-700 dark:text-blue-300 font-semibold">
|
||||
2
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<h4 class="text-base font-semibold text-gray-900 dark:text-white mb-2">
|
||||
Click the "Live" Button
|
||||
</h4>
|
||||
<p class="text-gray-600 dark:text-gray-400">
|
||||
The Live button has a distinctive green gradient style and will pulse when active
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-4">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="flex items-center justify-center w-8 h-8 rounded-full bg-blue-100 dark:bg-blue-900/50 text-blue-700 dark:text-blue-300 font-semibold">
|
||||
3
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<h4 class="text-base font-semibold text-gray-900 dark:text-white mb-2">
|
||||
Watch Real-Time Updates
|
||||
</h4>
|
||||
<p class="text-gray-600 dark:text-gray-400">
|
||||
The graph will start updating every second with fresh data. A pulsing green indicator
|
||||
shows that live polling is active.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-4">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="flex items-center justify-center w-8 h-8 rounded-full bg-blue-100 dark:bg-blue-900/50 text-blue-700 dark:text-blue-300 font-semibold">
|
||||
4
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<h4 class="text-base font-semibold text-gray-900 dark:text-white mb-2">
|
||||
Switch Back to Historical Data
|
||||
</h4>
|
||||
<p class="text-gray-600 dark:text-gray-400">
|
||||
Click any other time range button to stop live polling and view historical data
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mt-8 mb-3">
|
||||
Supported Metrics in Live Mode
|
||||
</h3>
|
||||
|
||||
<div class="mt-4 grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
<div class="p-3 bg-gray-50 dark:bg-gray-800/50 border border-gray-200 dark:border-white/10 rounded-lg">
|
||||
<h4 class="text-sm font-semibold text-gray-900 dark:text-white mb-1 flex items-center gap-2">
|
||||
<.icon name="hero-cpu-chip" class="h-4 w-4 text-blue-500" />
|
||||
CPU / Processors
|
||||
</h4>
|
||||
<p class="text-xs text-gray-600 dark:text-gray-400">
|
||||
Real-time CPU load and utilization
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="p-3 bg-gray-50 dark:bg-gray-800/50 border border-gray-200 dark:border-white/10 rounded-lg">
|
||||
<h4 class="text-sm font-semibold text-gray-900 dark:text-white mb-1 flex items-center gap-2">
|
||||
<.icon name="hero-circle-stack" class="h-4 w-4 text-blue-500" />
|
||||
Memory Usage
|
||||
</h4>
|
||||
<p class="text-xs text-gray-600 dark:text-gray-400">
|
||||
RAM utilization percentage
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="p-3 bg-gray-50 dark:bg-gray-800/50 border border-gray-200 dark:border-white/10 rounded-lg">
|
||||
<h4 class="text-sm font-semibold text-gray-900 dark:text-white mb-1 flex items-center gap-2">
|
||||
<.icon name="hero-fire" class="h-4 w-4 text-orange-500" /> Temperature
|
||||
</h4>
|
||||
<p class="text-xs text-gray-600 dark:text-gray-400">
|
||||
Device and component temperatures
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="p-3 bg-gray-50 dark:bg-gray-800/50 border border-gray-200 dark:border-white/10 rounded-lg">
|
||||
<h4 class="text-sm font-semibold text-gray-900 dark:text-white mb-1 flex items-center gap-2">
|
||||
<.icon name="hero-bolt" class="h-4 w-4 text-yellow-500" /> Voltage
|
||||
</h4>
|
||||
<p class="text-xs text-gray-600 dark:text-gray-400">
|
||||
Power supply voltages
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="p-3 bg-gray-50 dark:bg-gray-800/50 border border-gray-200 dark:border-white/10 rounded-lg">
|
||||
<h4 class="text-sm font-semibold text-gray-900 dark:text-white mb-1 flex items-center gap-2">
|
||||
<.icon name="hero-server-stack" class="h-4 w-4 text-purple-500" /> Storage
|
||||
</h4>
|
||||
<p class="text-xs text-gray-600 dark:text-gray-400">
|
||||
Disk usage and capacity
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="p-3 bg-gray-50 dark:bg-gray-800/50 border border-gray-200 dark:border-white/10 rounded-lg">
|
||||
<h4 class="text-sm font-semibold text-gray-900 dark:text-white mb-1 flex items-center gap-2">
|
||||
<.icon name="hero-hashtag" class="h-4 w-4 text-green-500" /> Custom Metrics
|
||||
</h4>
|
||||
<p class="text-xs text-gray-600 dark:text-gray-400">
|
||||
Sessions, connections, and counts
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 p-4 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg">
|
||||
<div class="flex gap-3">
|
||||
<.icon
|
||||
name="hero-information-circle"
|
||||
class="h-5 w-5 text-blue-600 dark:text-blue-400 flex-shrink-0 mt-0.5"
|
||||
/>
|
||||
<div>
|
||||
<h4 class="text-sm font-semibold text-blue-900 dark:text-blue-300 mb-1">
|
||||
Note About Traffic Graphs
|
||||
</h4>
|
||||
<p class="text-sm text-blue-800 dark:text-blue-300">
|
||||
Live mode is currently only available for sensor metrics (CPU, memory, temperature, etc.).
|
||||
Interface traffic graphs use historical data only, as traffic calculations require
|
||||
comparing multiple SNMP polls over time.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mt-8 mb-3">
|
||||
Tips for Using Graphs
|
||||
</h3>
|
||||
|
||||
<div class="space-y-3">
|
||||
<div class="p-4 bg-gray-50 dark:bg-gray-800/50 border border-gray-200 dark:border-white/10 rounded-lg">
|
||||
<h4 class="text-sm font-semibold text-gray-900 dark:text-white mb-2">
|
||||
Multiple Sensors on One Graph
|
||||
</h4>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||
When viewing aggregate metrics (like "Temperature" for all sensors), the graph
|
||||
automatically displays all sensors of that type with different colors for easy comparison.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="p-4 bg-gray-50 dark:bg-gray-800/50 border border-gray-200 dark:border-white/10 rounded-lg">
|
||||
<h4 class="text-sm font-semibold text-gray-900 dark:text-white mb-2">
|
||||
Max and Min Values
|
||||
</h4>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||
Historical graphs (non-live) display the maximum and minimum values for the selected
|
||||
time range at the bottom of the chart for quick reference.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="p-4 bg-gray-50 dark:bg-gray-800/50 border border-gray-200 dark:border-white/10 rounded-lg">
|
||||
<h4 class="text-sm font-semibold text-gray-900 dark:text-white mb-2">
|
||||
Traffic Graph Direction
|
||||
</h4>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||
Interface traffic graphs show outbound traffic as positive values (above zero) and
|
||||
inbound traffic as negative values (below zero) for easy visualization of bidirectional flow.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="p-4 bg-gray-50 dark:bg-gray-800/50 border border-gray-200 dark:border-white/10 rounded-lg">
|
||||
<h4 class="text-sm font-semibold text-gray-900 dark:text-white mb-2">
|
||||
Automatic Unit Scaling
|
||||
</h4>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||
Traffic graphs automatically scale units (bps, Kbps, Mbps, Gbps) based on the data
|
||||
range for optimal readability. Hover over data points to see exact values.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-8 p-4 bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg">
|
||||
<div class="flex gap-3">
|
||||
<.icon
|
||||
name="hero-light-bulb"
|
||||
class="h-5 w-5 text-yellow-600 dark:text-yellow-400 flex-shrink-0 mt-0.5"
|
||||
/>
|
||||
<div>
|
||||
<h4 class="text-sm font-semibold text-yellow-900 dark:text-yellow-300 mb-1">
|
||||
Performance Tip
|
||||
</h4>
|
||||
<p class="text-sm text-yellow-800 dark:text-yellow-300">
|
||||
Live polling makes direct SNMP requests every second. While this provides instant
|
||||
feedback, keeping many live graphs open simultaneously may impact device performance.
|
||||
Use live mode for troubleshooting and testing, then switch back to historical ranges
|
||||
for routine monitoring.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% "mikrotik" -> %>
|
||||
<div class="p-6">
|
||||
<h2 class="text-xl font-bold text-gray-900 dark:text-white mb-4">
|
||||
|
|
|
|||
313
test/towerops/contexts/config_change_tracker_test.exs
Normal file
313
test/towerops/contexts/config_change_tracker_test.exs
Normal file
|
|
@ -0,0 +1,313 @@
|
|||
defmodule Towerops.Contexts.ConfigChangeTrackerTest do
|
||||
use Towerops.DataCase, async: true
|
||||
|
||||
alias Towerops.Contexts.ConfigChangeTracker
|
||||
|
||||
describe "capture_config_state/1" do
|
||||
test "captures all config fields from organization struct" do
|
||||
org = %{
|
||||
snmp_community: "public",
|
||||
mikrotik_username: "admin",
|
||||
mikrotik_password: "password",
|
||||
mikrotik_port: 8729,
|
||||
mikrotik_use_ssl: true,
|
||||
mikrotik_enabled: true
|
||||
}
|
||||
|
||||
result = ConfigChangeTracker.capture_config_state(org)
|
||||
|
||||
assert result == %{
|
||||
snmp_community: "public",
|
||||
mikrotik_username: "admin",
|
||||
mikrotik_password: "password",
|
||||
mikrotik_port: 8729,
|
||||
mikrotik_use_ssl: true,
|
||||
mikrotik_enabled: true
|
||||
}
|
||||
end
|
||||
|
||||
test "captures partial config fields with nils" do
|
||||
site = %{
|
||||
snmp_community: "private",
|
||||
mikrotik_username: nil,
|
||||
mikrotik_password: nil,
|
||||
mikrotik_port: nil,
|
||||
mikrotik_use_ssl: nil,
|
||||
mikrotik_enabled: nil
|
||||
}
|
||||
|
||||
result = ConfigChangeTracker.capture_config_state(site)
|
||||
|
||||
assert result.snmp_community == "private"
|
||||
assert result.mikrotik_username == nil
|
||||
assert result.mikrotik_password == nil
|
||||
end
|
||||
|
||||
test "handles struct with missing fields gracefully" do
|
||||
minimal = %{snmp_community: "test"}
|
||||
|
||||
result = ConfigChangeTracker.capture_config_state(minimal)
|
||||
|
||||
assert result.snmp_community == "test"
|
||||
assert result.mikrotik_username == nil
|
||||
assert result.mikrotik_password == nil
|
||||
assert result.mikrotik_port == nil
|
||||
assert result.mikrotik_use_ssl == nil
|
||||
assert result.mikrotik_enabled == nil
|
||||
end
|
||||
end
|
||||
|
||||
describe "snmp_changed?/2" do
|
||||
test "returns true when SNMP community changed" do
|
||||
old_state = %{snmp_community: "public"}
|
||||
updated = %{snmp_community: "private"}
|
||||
|
||||
assert ConfigChangeTracker.snmp_changed?(updated, old_state) == true
|
||||
end
|
||||
|
||||
test "returns false when SNMP community unchanged" do
|
||||
old_state = %{snmp_community: "public"}
|
||||
updated = %{snmp_community: "public"}
|
||||
|
||||
assert ConfigChangeTracker.snmp_changed?(updated, old_state) == false
|
||||
end
|
||||
|
||||
test "returns true when SNMP community changes from nil to value" do
|
||||
old_state = %{snmp_community: nil}
|
||||
updated = %{snmp_community: "public"}
|
||||
|
||||
assert ConfigChangeTracker.snmp_changed?(updated, old_state) == true
|
||||
end
|
||||
|
||||
test "returns true when SNMP community changes from value to nil" do
|
||||
old_state = %{snmp_community: "public"}
|
||||
updated = %{snmp_community: nil}
|
||||
|
||||
assert ConfigChangeTracker.snmp_changed?(updated, old_state) == true
|
||||
end
|
||||
end
|
||||
|
||||
describe "mikrotik_changed?/2" do
|
||||
test "returns true when MikroTik username changed" do
|
||||
old_state = %{
|
||||
snmp_community: "public",
|
||||
mikrotik_username: "admin",
|
||||
mikrotik_password: "pass",
|
||||
mikrotik_port: 8729,
|
||||
mikrotik_use_ssl: true,
|
||||
mikrotik_enabled: true
|
||||
}
|
||||
|
||||
updated = %{
|
||||
snmp_community: "public",
|
||||
mikrotik_username: "root",
|
||||
mikrotik_password: "pass",
|
||||
mikrotik_port: 8729,
|
||||
mikrotik_use_ssl: true,
|
||||
mikrotik_enabled: true
|
||||
}
|
||||
|
||||
assert ConfigChangeTracker.mikrotik_changed?(updated, old_state) == true
|
||||
end
|
||||
|
||||
test "returns true when MikroTik password changed" do
|
||||
old_state = %{
|
||||
mikrotik_username: "admin",
|
||||
mikrotik_password: "oldpass",
|
||||
mikrotik_port: 8729,
|
||||
mikrotik_use_ssl: true,
|
||||
mikrotik_enabled: true
|
||||
}
|
||||
|
||||
updated = %{
|
||||
mikrotik_username: "admin",
|
||||
mikrotik_password: "newpass",
|
||||
mikrotik_port: 8729,
|
||||
mikrotik_use_ssl: true,
|
||||
mikrotik_enabled: true
|
||||
}
|
||||
|
||||
assert ConfigChangeTracker.mikrotik_changed?(updated, old_state) == true
|
||||
end
|
||||
|
||||
test "returns true when MikroTik port changed" do
|
||||
old_state = %{
|
||||
mikrotik_username: "admin",
|
||||
mikrotik_password: "pass",
|
||||
mikrotik_port: 8729,
|
||||
mikrotik_use_ssl: true,
|
||||
mikrotik_enabled: true
|
||||
}
|
||||
|
||||
updated = %{
|
||||
mikrotik_username: "admin",
|
||||
mikrotik_password: "pass",
|
||||
mikrotik_port: 8728,
|
||||
mikrotik_use_ssl: true,
|
||||
mikrotik_enabled: true
|
||||
}
|
||||
|
||||
assert ConfigChangeTracker.mikrotik_changed?(updated, old_state) == true
|
||||
end
|
||||
|
||||
test "returns true when MikroTik SSL setting changed" do
|
||||
old_state = %{
|
||||
mikrotik_username: "admin",
|
||||
mikrotik_password: "pass",
|
||||
mikrotik_port: 8729,
|
||||
mikrotik_use_ssl: true,
|
||||
mikrotik_enabled: true
|
||||
}
|
||||
|
||||
updated = %{
|
||||
mikrotik_username: "admin",
|
||||
mikrotik_password: "pass",
|
||||
mikrotik_port: 8729,
|
||||
mikrotik_use_ssl: false,
|
||||
mikrotik_enabled: true
|
||||
}
|
||||
|
||||
assert ConfigChangeTracker.mikrotik_changed?(updated, old_state) == true
|
||||
end
|
||||
|
||||
test "returns true when MikroTik enabled setting changed" do
|
||||
old_state = %{
|
||||
mikrotik_username: "admin",
|
||||
mikrotik_password: "pass",
|
||||
mikrotik_port: 8729,
|
||||
mikrotik_use_ssl: true,
|
||||
mikrotik_enabled: false
|
||||
}
|
||||
|
||||
updated = %{
|
||||
mikrotik_username: "admin",
|
||||
mikrotik_password: "pass",
|
||||
mikrotik_port: 8729,
|
||||
mikrotik_use_ssl: true,
|
||||
mikrotik_enabled: true
|
||||
}
|
||||
|
||||
assert ConfigChangeTracker.mikrotik_changed?(updated, old_state) == true
|
||||
end
|
||||
|
||||
test "returns false when no MikroTik fields changed" do
|
||||
old_state = %{
|
||||
snmp_community: "public",
|
||||
mikrotik_username: "admin",
|
||||
mikrotik_password: "pass",
|
||||
mikrotik_port: 8729,
|
||||
mikrotik_use_ssl: true,
|
||||
mikrotik_enabled: true
|
||||
}
|
||||
|
||||
updated = %{
|
||||
snmp_community: "public",
|
||||
mikrotik_username: "admin",
|
||||
mikrotik_password: "pass",
|
||||
mikrotik_port: 8729,
|
||||
mikrotik_use_ssl: true,
|
||||
mikrotik_enabled: true
|
||||
}
|
||||
|
||||
assert ConfigChangeTracker.mikrotik_changed?(updated, old_state) == false
|
||||
end
|
||||
|
||||
test "returns false when only SNMP community changed (not MikroTik field)" do
|
||||
old_state = %{
|
||||
snmp_community: "public",
|
||||
mikrotik_username: "admin",
|
||||
mikrotik_password: "pass",
|
||||
mikrotik_port: 8729,
|
||||
mikrotik_use_ssl: true,
|
||||
mikrotik_enabled: true
|
||||
}
|
||||
|
||||
updated = %{
|
||||
snmp_community: "private",
|
||||
mikrotik_username: "admin",
|
||||
mikrotik_password: "pass",
|
||||
mikrotik_port: 8729,
|
||||
mikrotik_use_ssl: true,
|
||||
mikrotik_enabled: true
|
||||
}
|
||||
|
||||
assert ConfigChangeTracker.mikrotik_changed?(updated, old_state) == false
|
||||
end
|
||||
|
||||
test "returns true when MikroTik field changes from nil to value" do
|
||||
old_state = %{
|
||||
mikrotik_username: nil,
|
||||
mikrotik_password: nil,
|
||||
mikrotik_port: nil,
|
||||
mikrotik_use_ssl: nil,
|
||||
mikrotik_enabled: nil
|
||||
}
|
||||
|
||||
updated = %{
|
||||
mikrotik_username: "admin",
|
||||
mikrotik_password: nil,
|
||||
mikrotik_port: nil,
|
||||
mikrotik_use_ssl: nil,
|
||||
mikrotik_enabled: nil
|
||||
}
|
||||
|
||||
assert ConfigChangeTracker.mikrotik_changed?(updated, old_state) == true
|
||||
end
|
||||
end
|
||||
|
||||
describe "extract_mikrotik_attrs/1" do
|
||||
test "extracts all MikroTik fields from struct" do
|
||||
struct = %{
|
||||
mikrotik_username: "admin",
|
||||
mikrotik_password: "password",
|
||||
mikrotik_port: 8729,
|
||||
mikrotik_use_ssl: true,
|
||||
mikrotik_enabled: true,
|
||||
snmp_community: "public"
|
||||
}
|
||||
|
||||
result = ConfigChangeTracker.extract_mikrotik_attrs(struct)
|
||||
|
||||
assert result == %{
|
||||
mikrotik_username: "admin",
|
||||
mikrotik_password: "password",
|
||||
mikrotik_port: 8729,
|
||||
mikrotik_use_ssl: true,
|
||||
mikrotik_enabled: true
|
||||
}
|
||||
end
|
||||
|
||||
test "extracts MikroTik fields with nils" do
|
||||
struct = %{
|
||||
mikrotik_username: "admin",
|
||||
mikrotik_password: nil,
|
||||
mikrotik_port: nil,
|
||||
mikrotik_use_ssl: nil,
|
||||
mikrotik_enabled: false
|
||||
}
|
||||
|
||||
result = ConfigChangeTracker.extract_mikrotik_attrs(struct)
|
||||
|
||||
assert result.mikrotik_username == "admin"
|
||||
assert result.mikrotik_password == nil
|
||||
assert result.mikrotik_port == nil
|
||||
assert result.mikrotik_use_ssl == nil
|
||||
assert result.mikrotik_enabled == false
|
||||
end
|
||||
|
||||
test "does not include SNMP community field" do
|
||||
struct = %{
|
||||
snmp_community: "public",
|
||||
mikrotik_username: "admin",
|
||||
mikrotik_password: "pass",
|
||||
mikrotik_port: 8729,
|
||||
mikrotik_use_ssl: true,
|
||||
mikrotik_enabled: true
|
||||
}
|
||||
|
||||
result = ConfigChangeTracker.extract_mikrotik_attrs(struct)
|
||||
|
||||
refute Map.has_key?(result, :snmp_community)
|
||||
end
|
||||
end
|
||||
end
|
||||
Loading…
Add table
Reference in a new issue