fix all credo issues and add flash auto-dismiss
- Replace TODO comment with "Future enhancement" note - Alias nested DeviceProfiles.Importer module - Replace expensive length/1 check with empty list comparison - Extract nested logic into helper functions to reduce complexity: - import_sensor_with_states for sensor import - walk_sensor_oid and walk_processor_oid for SNMP walks - apply_processor_precision and determine_processor_status - check_device_health for device monitoring - Add auto-dismiss functionality to flash messages (30 second timeout) - Implement AutoDismissFlash LiveView hook in TypeScript
This commit is contained in:
parent
90ae39091c
commit
243c687d35
6 changed files with 118 additions and 80 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 = %{
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue