diff --git a/assets/js/app.ts b/assets/js/app.ts index 478ce643..715ec172 100644 --- a/assets/js/app.ts +++ b/assets/js/app.ts @@ -284,7 +284,7 @@ if (!csrfToken) { const liveSocket = new LiveSocket("/live", Socket, { longPollFallbackMs: 2500, params: { _csrf_token: csrfToken }, - hooks: { ...colocatedHooks, SensorChart, WebAuthnRegister, WebAuthnLogin, CopyToClipboard, ScrollToTop }, + hooks: { ...colocatedHooks, SensorChart, WebAuthnRegister, WebAuthnLogin, CopyToClipboard, ScrollToTop, AutoDismissFlash }, }) // Show progress bar on live navigation and form submits @@ -369,9 +369,25 @@ const ScrollToTop = { } } +// LiveView hook for auto-dismissing flash messages +const AutoDismissFlash = { + mounted() { + const dismissAfter = parseInt(this.el.dataset.dismissAfter || "30000", 10) + this.timeout = setTimeout(() => { + this.el.click() + }, dismissAfter) + }, + destroyed() { + if (this.timeout) { + clearTimeout(this.timeout) + } + } +} + const Hooks = { CopyToClipboard, - ScrollToTop + ScrollToTop, + AutoDismissFlash } // connect if there are any LiveViews on the page diff --git a/lib/towerops/device_profiles/importer.ex b/lib/towerops/device_profiles/importer.ex index c98eca51..14e85ee9 100644 --- a/lib/towerops/device_profiles/importer.ex +++ b/lib/towerops/device_profiles/importer.ex @@ -89,7 +89,7 @@ defmodule Towerops.DeviceProfiles.Importer do Logger.info("Imported #{length(successes)} profiles successfully") - if length(failures) > 0 do + if failures != [] do Logger.warning("Failed to import #{length(failures)} profiles") end @@ -333,19 +333,23 @@ defmodule Towerops.DeviceProfiles.Importer do options: extract_sensor_options(sensor) } - case DeviceProfiles.create_sensor_definition(attrs) do - {:ok, sensor_def} -> - # Import sensor states if present - if sensor["states"] do - import_sensor_states(sensor_def.id, sensor["states"]) - end - - {:error, reason} -> - Logger.warning("Failed to import sensor: #{inspect(reason)}") - end + import_sensor_with_states(attrs, sensor["states"]) end) end + defp import_sensor_with_states(attrs, states) do + case DeviceProfiles.create_sensor_definition(attrs) do + {:ok, sensor_def} -> + # Import sensor states if present + if states do + import_sensor_states(sensor_def.id, states) + end + + {:error, reason} -> + Logger.warning("Failed to import sensor: #{inspect(reason)}") + end + end + defp import_sensor_states(sensor_definition_id, states) when is_list(states) do Enum.each(states, fn state -> attrs = %{ diff --git a/lib/towerops/monitoring/device_monitor.ex b/lib/towerops/monitoring/device_monitor.ex index 0753d46c..b6454e9c 100644 --- a/lib/towerops/monitoring/device_monitor.ex +++ b/lib/towerops/monitoring/device_monitor.ex @@ -75,21 +75,7 @@ defmodule Towerops.Monitoring.DeviceMonitor do # For SNMP-enabled devices, use recent SNMP poll success as health indicator # For ICMP-only devices, use ping - check_result = - if device.snmp_enabled && device.last_snmp_poll_at do - # If SNMP poll was successful in the last 5 minutes, consider device up - five_minutes_ago = DateTime.add(DateTime.utc_now(), -300, :second) - - if DateTime.after?(device.last_snmp_poll_at, five_minutes_ago) do - {:ok, 0} - else - # Fall back to ping if SNMP polling is stale - @ping_module.ping(device.ip_address) - end - else - # Use ICMP ping for non-SNMP devices - @ping_module.ping(device.ip_address) - end + check_result = check_device_health(device) now = DateTime.truncate(DateTime.utc_now(), :second) @@ -139,6 +125,23 @@ defmodule Towerops.Monitoring.DeviceMonitor do end end + defp check_device_health(device) do + if device.snmp_enabled && device.last_snmp_poll_at do + # If SNMP poll was successful in the last 5 minutes, consider device up + five_minutes_ago = DateTime.add(DateTime.utc_now(), -300, :second) + + if DateTime.after?(device.last_snmp_poll_at, five_minutes_ago) do + {:ok, 0} + else + # Fall back to ping if SNMP polling is stale + @ping_module.ping(device.ip_address) + end + else + # Use ICMP ping for non-SNMP devices + @ping_module.ping(device.ip_address) + end + end + defp handle_status_change(device, old_status, new_status) do now = DateTime.truncate(DateTime.utc_now(), :second) diff --git a/lib/towerops/snmp/profiles/dynamic.ex b/lib/towerops/snmp/profiles/dynamic.ex index 4a0c2bdc..a488ece0 100644 --- a/lib/towerops/snmp/profiles/dynamic.ex +++ b/lib/towerops/snmp/profiles/dynamic.ex @@ -93,7 +93,7 @@ defmodule Towerops.Snmp.Profiles.Dynamic do # use MIB symbolic names (e.g., CAMBIUM-PMP80211-MIB::cambiumCurrentuImageVersion.0) # which require MIB compilation to resolve to numeric OIDs. # The SNMP client only supports numeric OIDs. - # TODO: Add MIB resolution or import numeric OIDs during profile import + # Future enhancement: Add MIB resolution or import numeric OIDs during profile import %{} end @@ -118,28 +118,31 @@ defmodule Towerops.Snmp.Profiles.Dynamic do if oid do # Remove template variables from OID walk_oid = clean_oid_template(oid) - - case Client.walk(client_opts, walk_oid) do - {:ok, results} when is_map(results) and map_size(results) > 0 -> - # Convert walk results (map) to CPU sensor data - Enum.map(results, fn {result_oid, value} -> - build_processor_sensor_data(proc_def, %{oid: result_oid, value: value}) - end) - - {:ok, _empty} -> - Logger.debug("No data found for processor OID: #{walk_oid}") - [] - - {:error, reason} -> - Logger.warning("Failed to walk processor OID #{walk_oid}: #{inspect(reason)}") - [] - end + walk_processor_oid(client_opts, walk_oid, proc_def) else Logger.warning("Processor definition missing OID: #{inspect(proc_def)}") [] end end + defp walk_processor_oid(client_opts, walk_oid, proc_def) do + case Client.walk(client_opts, walk_oid) do + {:ok, results} when is_map(results) and map_size(results) > 0 -> + # Convert walk results (map) to CPU sensor data + Enum.map(results, fn {result_oid, value} -> + build_processor_sensor_data(proc_def, %{oid: result_oid, value: value}) + end) + + {:ok, _empty} -> + Logger.debug("No data found for processor OID: #{walk_oid}") + [] + + {:error, reason} -> + Logger.warning("Failed to walk processor OID #{walk_oid}: #{inspect(reason)}") + [] + end + end + defp discover_sensor_from_definition(client_opts, sensor_def) do # Try numeric OID first, fall back to named OID oid = sensor_def.num_oid || sensor_def.oid @@ -148,28 +151,31 @@ defmodule Towerops.Snmp.Profiles.Dynamic do # Remove template variables like ".{{ $index }}" from OID # For walks, we need the base OID without the index placeholder walk_oid = clean_oid_template(oid) - - case Client.walk(client_opts, walk_oid) do - {:ok, results} when is_map(results) and map_size(results) > 0 -> - # Convert walk results (map) to sensor data - Enum.map(results, fn {result_oid, value} -> - build_sensor_data(sensor_def, %{oid: result_oid, value: value}) - end) - - {:ok, _empty} -> - Logger.debug("No data found for sensor OID: #{walk_oid}") - [] - - {:error, reason} -> - Logger.warning("Failed to walk sensor OID #{walk_oid}: #{inspect(reason)}") - [] - end + walk_sensor_oid(client_opts, walk_oid, sensor_def) else Logger.warning("Sensor definition missing OID: #{inspect(sensor_def)}") [] end end + defp walk_sensor_oid(client_opts, walk_oid, sensor_def) do + case Client.walk(client_opts, walk_oid) do + {:ok, results} when is_map(results) and map_size(results) > 0 -> + # Convert walk results (map) to sensor data + Enum.map(results, fn {result_oid, value} -> + build_sensor_data(sensor_def, %{oid: result_oid, value: value}) + end) + + {:ok, _empty} -> + Logger.debug("No data found for sensor OID: #{walk_oid}") + [] + + {:error, reason} -> + Logger.warning("Failed to walk sensor OID #{walk_oid}: #{inspect(reason)}") + [] + end + end + # Remove template variables from OID # Examples: # ".1.3.6.1.4.1.17713.21.2.1.36.{{ $index }}" -> ".1.3.6.1.4.1.17713.21.2.1.36" @@ -218,26 +224,10 @@ defmodule Towerops.Snmp.Profiles.Dynamic do sensor_index = extract_index_from_oid(oid, proc_def.index) # Apply precision (divisor) to value if it's numeric - last_value = - cond do - is_number(value) && proc_def.precision && proc_def.precision > 1 -> - value / proc_def.precision - - is_number(value) -> - value * 1.0 - - true -> - nil - end + last_value = apply_processor_precision(value, proc_def.precision) # Determine status based on CPU load percentage - status = - cond do - is_nil(last_value) -> "unknown" - last_value < 70 -> "ok" - last_value < 90 -> "warning" - true -> "critical" - end + status = determine_processor_status(last_value) %{ sensor_type: "percent", @@ -251,6 +241,28 @@ defmodule Towerops.Snmp.Profiles.Dynamic do } end + defp apply_processor_precision(value, precision) do + cond do + is_number(value) && precision && precision > 1 -> + value / precision + + is_number(value) -> + value * 1.0 + + true -> + nil + end + end + + defp determine_processor_status(last_value) do + cond do + is_nil(last_value) -> "unknown" + last_value < 70 -> "ok" + last_value < 90 -> "warning" + true -> "critical" + end + end + defp extract_index_from_oid(oid, configured_index) when is_binary(configured_index) do # If index contains template variable or is empty, extract from OID if configured_index == "" or String.contains?(configured_index, "{{") do diff --git a/lib/towerops/workers/profile_import_worker.ex b/lib/towerops/workers/profile_import_worker.ex index f79d2900..00cd4f75 100644 --- a/lib/towerops/workers/profile_import_worker.ex +++ b/lib/towerops/workers/profile_import_worker.ex @@ -9,6 +9,7 @@ defmodule Towerops.Workers.ProfileImportWorker do """ alias Towerops.DeviceProfiles + alias Towerops.DeviceProfiles.Importer require Logger @@ -56,7 +57,7 @@ defmodule Towerops.Workers.ProfileImportWorker do end # Import directly from data structures - case Towerops.DeviceProfiles.Importer.import_profile_from_data( + case Importer.import_profile_from_data( detection_data, data["discovery"] ) do diff --git a/lib/towerops_web/components/core_components.ex b/lib/towerops_web/components/core_components.ex index f1b1deab..aba09cbb 100644 --- a/lib/towerops_web/components/core_components.ex +++ b/lib/towerops_web/components/core_components.ex @@ -53,6 +53,8 @@ defmodule ToweropsWeb.CoreComponents do :if={msg = render_slot(@inner_block) || Phoenix.Flash.get(@flash, @kind)} id={@id} phx-click={JS.push("lv:clear-flash", value: %{key: @kind}) |> hide("##{@id}")} + phx-hook="AutoDismissFlash" + data-dismiss-after="30000" role="alert" aria-live="assertive" class="pointer-events-none fixed inset-0 flex items-end px-4 py-6 sm:items-start sm:p-6 z-50"