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:
Graham McIntire 2026-01-18 10:43:23 -06:00
parent 90ae39091c
commit 243c687d35
No known key found for this signature in database
6 changed files with 118 additions and 80 deletions

View file

@ -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

View file

@ -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 = %{

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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"