diff --git a/lib/towerops/contexts/config_change_tracker.ex b/lib/towerops/contexts/config_change_tracker.ex new file mode 100644 index 00000000..575c908c --- /dev/null +++ b/lib/towerops/contexts/config_change_tracker.ex @@ -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 diff --git a/lib/towerops/organizations.ex b/lib/towerops/organizations.ex index 4c048d28..ba50b11e 100644 --- a/lib/towerops/organizations.ex +++ b/lib/towerops/organizations.ex @@ -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 diff --git a/lib/towerops/sites.ex b/lib/towerops/sites.ex index e2bb7725..d57684ee 100644 --- a/lib/towerops/sites.ex +++ b/lib/towerops/sites.ex @@ -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 diff --git a/lib/towerops_web/controllers/api_docs_html/index.html.heex b/lib/towerops_web/controllers/api_docs_html/index.html.heex index 8f113fd4..75dd4b59 100644 --- a/lib/towerops_web/controllers/api_docs_html/index.html.heex +++ b/lib/towerops_web/controllers/api_docs_html/index.html.heex @@ -76,6 +76,15 @@ Account Data (GDPR) +
+ 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. +
+ ++ Graphs are available throughout Towerops wherever metrics are displayed: +
+ ++ All graphs support multiple time ranges for analyzing trends at different scales: +
+ ++ Recent activity with high detail +
++ Half-day trends and patterns +
++ Business day overview +
++ Full day of activity +
++ Weekly trends and patterns +
++ Monthly overview and capacity planning +
++ Live mode provides real-time sensor monitoring with data updating every second. This is perfect for: +
+ ++ Navigate to any device and click on a metric graph (CPU, Memory, Temperature, Traffic, etc.) +
++ The Live button has a distinctive green gradient style and will pulse when active +
++ The graph will start updating every second with fresh data. A pulsing green indicator + shows that live polling is active. +
++ Click any other time range button to stop live polling and view historical data +
++ Real-time CPU load and utilization +
++ RAM utilization percentage +
++ Device and component temperatures +
++ Power supply voltages +
++ Disk usage and capacity +
++ Sessions, connections, and counts +
++ 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. +
++ When viewing aggregate metrics (like "Temperature" for all sensors), the graph + automatically displays all sensors of that type with different colors for easy comparison. +
++ Historical graphs (non-live) display the maximum and minimum values for the selected + time range at the bottom of the chart for quick reference. +
++ 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. +
++ 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. +
++ 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. +
+