fix: resolve all dialyzer unmatched return and pattern match warnings

Fixed dialyzer warnings across multiple files by properly handling return
values from functions that are called for side effects.

Changes:
- lib/towerops/agents.ex: Explicitly match broadcast and if-expression returns
- lib/towerops/devices.ex: Match worker start function returns in if-expressions
- lib/towerops/workers/agent_latency_evaluator.ex: Match broadcast return
- lib/towerops/workers/device_monitor_worker.ex: Match perform_check and handle_status_change returns, handle schedule_next_check errors
- lib/towerops/workers/device_poller_worker.ex: Match all PubSub broadcast returns, handle schedule_next_poll errors, remove unreachable pattern
- lib/towerops/workers/stale_agent_worker.ex: Match process_stale_agents and broadcast returns
- lib/towerops_web/channels/agent_channel.ex: Match PubSub.subscribe return, improve get_addr pattern matching, remove unreachable format_ip clause
- lib/towerops_web/live/device_live/form.ex: Match enqueue_discovery return, replace compile-time @dev_env with runtime check

Pattern applied:
- PubSub broadcasts: `_ = Phoenix.PubSub.broadcast(...)`
- If-expressions with side effects: `_ = if condition do ... end`
- Critical operations: Added error logging with case statements

All 3625 tests passing. No dialyzer warnings remaining in lib/towerops or lib/towerops_web.

🤖 Generated with Claude Code

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Graham McIntire 2026-01-24 17:34:37 -06:00
parent 2e5faf7159
commit 6c1f344b5e
No known key found for this signature in database
9 changed files with 129 additions and 119 deletions

View file

@ -1,24 +1,3 @@
# Dialyzer warnings to ignore
# These are either false positives or intentional design choices
[
# SnmpKit library warnings (third-party code in lib/snmpkit/)
~r/lib\/snmpkit\//,
# Unmatched return values (intentionally ignored for fire-and-forget operations)
{"lib/towerops/agents.ex", :unmatched_return},
{"lib/towerops/devices.ex", :unmatched_return},
{"lib/towerops/workers/agent_latency_evaluator.ex", :unmatched_return},
{"lib/towerops/workers/device_monitor_worker.ex", :unmatched_return},
{"lib/towerops/workers/device_poller_worker.ex", :unmatched_return},
{"lib/towerops/workers/stale_agent_worker.ex", :unmatched_return},
{"lib/towerops_web/channels/agent_channel.ex", :unmatched_return},
{"lib/towerops_web/live/device_live/form.ex", :unmatched_return},
# Pattern match coverage warnings (intentional exhaustive patterns)
{"lib/towerops/workers/device_poller_worker.ex", :pattern_match_cov},
{"lib/towerops_web/channels/agent_channel.ex", :pattern_match_cov},
# Guard failures and unused functions in form helpers
{"lib/towerops_web/live/device_live/form.ex", :guard_fail},
{"lib/towerops_web/live/device_live/form.ex", :unused_fun}
]
# Dialyzer false positives to ignore
# Fix all warnings - do not suppress
[]

View file

@ -294,7 +294,7 @@ defmodule Towerops.Agents do
case result do
{:ok, assignment} ->
broadcast_assignment_change(agent_token_id, :device_assigned)
_ = broadcast_assignment_change(agent_token_id, :device_assigned)
{:ok, assignment}
error ->
@ -321,9 +321,10 @@ defmodule Towerops.Agents do
|> Repo.delete_all()
# Notify the agent that was previously assigned
if current_assignment do
broadcast_assignment_change(current_assignment.agent_token_id, :device_unassigned)
end
_ =
if current_assignment do
broadcast_assignment_change(current_assignment.agent_token_id, :device_unassigned)
end
result
end
@ -466,10 +467,11 @@ defmodule Towerops.Agents do
case result do
{:ok, assignment} ->
# Notify both the old and new agents if they're different
if old_agent_id != agent_token_id do
broadcast_assignment_change(old_agent_id, :device_unassigned)
broadcast_assignment_change(agent_token_id, :device_assigned)
end
_ =
if old_agent_id != agent_token_id do
_ = broadcast_assignment_change(old_agent_id, :device_unassigned)
broadcast_assignment_change(agent_token_id, :device_assigned)
end
{:ok, assignment}

View file

@ -259,13 +259,15 @@ defmodule Towerops.Devices do
|> Repo.insert() do
{:ok, device} = result ->
# Start monitoring/polling if enabled
if device.monitoring_enabled do
DeviceMonitorWorker.start_monitoring(device.id)
end
_ =
if device.monitoring_enabled do
DeviceMonitorWorker.start_monitoring(device.id)
end
if device.snmp_enabled do
DevicePollerWorker.start_polling(device.id)
end
_ =
if device.snmp_enabled do
DevicePollerWorker.start_polling(device.id)
end
result
@ -285,8 +287,8 @@ defmodule Towerops.Devices do
|> DeviceSchema.changeset(attrs)
|> Repo.update() do
{:ok, updated_device} = result ->
handle_monitoring_changes(updated_device, old_monitoring)
handle_snmp_changes(updated_device, old_snmp)
_ = handle_monitoring_changes(updated_device, old_monitoring)
_ = handle_snmp_changes(updated_device, old_snmp)
result
error ->

View file

@ -80,7 +80,7 @@ defmodule Towerops.Workers.AgentLatencyEvaluator do
case result do
{:ok, _} ->
broadcast_reassignment(candidate)
_ = broadcast_reassignment(candidate)
:ok
{:error, reason} ->

View file

@ -33,12 +33,21 @@ defmodule Towerops.Workers.DeviceMonitorWorker do
:ok
device ->
if device.monitoring_enabled do
perform_check(device)
end
_ =
if device.monitoring_enabled do
perform_check(device)
end
# Schedule next check
schedule_next_check(device_id)
case schedule_next_check(device_id) do
{:ok, _job} ->
:ok
{:error, changeset} ->
Logger.error("Failed to schedule next monitoring check for device #{device_id}: #{inspect(changeset.errors)}")
:ok
end
:ok
end
@ -108,16 +117,18 @@ defmodule Towerops.Workers.DeviceMonitorWorker do
_ = Devices.update_device_status(device, new_status)
if old_status != new_status do
handle_status_change(device, old_status, new_status)
end
_ =
if old_status != new_status do
handle_status_change(device, old_status, new_status)
end
# Broadcast status change via PubSub
Phoenix.PubSub.broadcast(
Towerops.PubSub,
"device:#{device.id}",
{:device_status_changed, device.id, new_status, nil}
)
_ =
Phoenix.PubSub.broadcast(
Towerops.PubSub,
"device:#{device.id}",
{:device_status_changed, device.id, new_status, nil}
)
end
defp check_device_health(device) do
@ -155,11 +166,12 @@ defmodule Towerops.Workers.DeviceMonitorWorker do
message: alert_message
})
Phoenix.PubSub.broadcast(
Towerops.PubSub,
"alerts:new",
{:new_alert, device.id, :device_down}
)
_ =
Phoenix.PubSub.broadcast(
Towerops.PubSub,
"alerts:new",
{:new_alert, device.id, :device_down}
)
end
defp get_down_alert_message(device) do
@ -183,11 +195,12 @@ defmodule Towerops.Workers.DeviceMonitorWorker do
resolve_down_alert(device)
Phoenix.PubSub.broadcast(
Towerops.PubSub,
"alerts:resolved",
{:alert_resolved, device.id, :device_down}
)
_ =
Phoenix.PubSub.broadcast(
Towerops.PubSub,
"alerts:resolved",
{:alert_resolved, device.id, :device_down}
)
end
defp get_recovery_message(device) do

View file

@ -45,7 +45,16 @@ defmodule Towerops.Workers.DevicePollerWorker do
# Schedule next poll
poll_interval = get_poll_interval(device)
schedule_next_poll(device_id, poll_interval)
case schedule_next_poll(device_id, poll_interval) do
{:ok, _job} ->
:ok
{:error, changeset} ->
Logger.error("Failed to schedule next poll for device #{device_id}: #{inspect(changeset.errors)}")
:ok
end
:ok
end
@ -136,11 +145,12 @@ defmodule Towerops.Workers.DevicePollerWorker do
poll_sensors(snmp_device.sensors, client_opts, now)
Logger.debug("Polled #{length(snmp_device.sensors)} sensors for #{device.name}")
Phoenix.PubSub.broadcast(
Towerops.PubSub,
"device:#{device.id}",
{:sensors_updated, device.id}
)
_ =
Phoenix.PubSub.broadcast(
Towerops.PubSub,
"device:#{device.id}",
{:sensors_updated, device.id}
)
rescue
error ->
Logger.error("Error polling sensors for #{device.name}: #{inspect(error)}\n#{Exception.format_stacktrace()}")
@ -150,11 +160,12 @@ defmodule Towerops.Workers.DevicePollerWorker do
poll_state_sensors(snmp_device.state_sensors, client_opts, now, device.id)
Logger.debug("Polled #{length(snmp_device.state_sensors)} state sensors for #{device.name}")
Phoenix.PubSub.broadcast(
Towerops.PubSub,
"device:#{device.id}",
{:state_sensors_updated, device.id}
)
_ =
Phoenix.PubSub.broadcast(
Towerops.PubSub,
"device:#{device.id}",
{:state_sensors_updated, device.id}
)
rescue
error ->
Logger.error("Error polling state sensors for #{device.name}: #{inspect(error)}\n#{Exception.format_stacktrace()}")
@ -164,11 +175,12 @@ defmodule Towerops.Workers.DevicePollerWorker do
poll_interfaces(snmp_device.interfaces, client_opts, now)
Logger.debug("Polled #{length(snmp_device.interfaces)} interfaces for #{device.name}")
Phoenix.PubSub.broadcast(
Towerops.PubSub,
"device:#{device.id}",
{:interfaces_updated, device.id}
)
_ =
Phoenix.PubSub.broadcast(
Towerops.PubSub,
"device:#{device.id}",
{:interfaces_updated, device.id}
)
rescue
error ->
Logger.error("Error polling interfaces for #{device.name}: #{inspect(error)}\n#{Exception.format_stacktrace()}")
@ -197,11 +209,12 @@ defmodule Towerops.Workers.DevicePollerWorker do
Logger.debug("Polled and saved #{length(neighbors)} neighbors for #{device.name}")
Phoenix.PubSub.broadcast(
Towerops.PubSub,
"device:#{device.id}",
{:neighbors_updated, device.id}
)
_ =
Phoenix.PubSub.broadcast(
Towerops.PubSub,
"device:#{device.id}",
{:neighbors_updated, device.id}
)
rescue
error ->
Logger.error("Error polling neighbors for #{device.name}: #{inspect(error)}\n#{Exception.format_stacktrace()}")
@ -217,11 +230,12 @@ defmodule Towerops.Workers.DevicePollerWorker do
Logger.debug("Polled and saved #{length(arp_entries)} ARP entries for #{device.name}")
Phoenix.PubSub.broadcast(
Towerops.PubSub,
"device:#{device.id}",
{:arp_updated, device.id}
)
_ =
Phoenix.PubSub.broadcast(
Towerops.PubSub,
"device:#{device.id}",
{:arp_updated, device.id}
)
rescue
error ->
Logger.error("Error polling ARP table for #{device.name}: #{inspect(error)}\n#{Exception.format_stacktrace()}")
@ -237,11 +251,12 @@ defmodule Towerops.Workers.DevicePollerWorker do
Logger.debug("Polled and saved #{length(mac_entries)} MAC FDB entries for #{device.name}")
Phoenix.PubSub.broadcast(
Towerops.PubSub,
"device:#{device.id}",
{:mac_updated, device.id}
)
_ =
Phoenix.PubSub.broadcast(
Towerops.PubSub,
"device:#{device.id}",
{:mac_updated, device.id}
)
rescue
error ->
Logger.error("Error polling MAC FDB table for #{device.name}: #{inspect(error)}\n#{Exception.format_stacktrace()}")
@ -254,11 +269,12 @@ defmodule Towerops.Workers.DevicePollerWorker do
poll_processors(processors, client_opts, now)
Logger.debug("Polled processors for #{device.name}")
Phoenix.PubSub.broadcast(
Towerops.PubSub,
"device:#{device.id}",
{:processors_updated, device.id}
)
_ =
Phoenix.PubSub.broadcast(
Towerops.PubSub,
"device:#{device.id}",
{:processors_updated, device.id}
)
end
rescue
error ->
@ -272,11 +288,12 @@ defmodule Towerops.Workers.DevicePollerWorker do
poll_storage(storage_entries, client_opts, now)
Logger.debug("Polled #{length(storage_entries)} storage entries for #{device.name}")
Phoenix.PubSub.broadcast(
Towerops.PubSub,
"device:#{device.id}",
{:storage_updated, device.id}
)
_ =
Phoenix.PubSub.broadcast(
Towerops.PubSub,
"device:#{device.id}",
{:storage_updated, device.id}
)
end
rescue
error ->
@ -318,7 +335,6 @@ defmodule Towerops.Workers.DevicePollerWorker do
else
{:error, reason} -> {:error, reason}
false -> {:error, :invalid_values}
_ -> {:error, :unknown}
end
end

View file

@ -31,9 +31,10 @@ defmodule Towerops.Workers.StaleAgentWorker do
def perform(%Oban.Job{}) do
stale_agents = find_stale_agents()
if !Enum.empty?(stale_agents) do
process_stale_agents(stale_agents)
end
_ =
if !Enum.empty?(stale_agents) do
process_stale_agents(stale_agents)
end
:ok
end
@ -50,7 +51,7 @@ defmodule Towerops.Workers.StaleAgentWorker do
Enum.each(long_term_stale, fn agent -> log_stale_agent(agent, :debug) end)
# Broadcast all stale agents for UI updates
Phoenix.PubSub.broadcast(Towerops.PubSub, "agents:health", {:agents_stale, stale_agents})
_ = Phoenix.PubSub.broadcast(Towerops.PubSub, "agents:health", {:agents_stale, stale_agents})
end
defp log_newly_stale_agents([]), do: :ok

View file

@ -50,7 +50,7 @@ defmodule ToweropsWeb.AgentChannel do
|> assign(:organization_id, agent_token.organization_id)
# Subscribe to assignment changes for this agent
Phoenix.PubSub.subscribe(Towerops.PubSub, "agent:#{agent_token.id}:assignments")
_ = Phoenix.PubSub.subscribe(Towerops.PubSub, "agent:#{agent_token.id}:assignments")
# Update last_seen_at and IP on join
remote_ip = get_remote_ip(socket)
@ -463,7 +463,7 @@ defmodule ToweropsWeb.AgentChannel do
pid when is_pid(pid) ->
case :ranch.get_addr(pid) do
{ip, _port} -> format_ip(ip)
{ip, _port} when is_tuple(ip) -> format_ip(ip)
_ -> nil
end
end
@ -476,6 +476,4 @@ defmodule ToweropsWeb.AgentChannel do
defp format_ip({a, b, c, d, e, f, g, h}),
do:
"#{Integer.to_string(a, 16)}:#{Integer.to_string(b, 16)}:#{Integer.to_string(c, 16)}:#{Integer.to_string(d, 16)}:#{Integer.to_string(e, 16)}:#{Integer.to_string(f, 16)}:#{Integer.to_string(g, 16)}:#{Integer.to_string(h, 16)}"
defp format_ip(_), do: nil
end

View file

@ -314,9 +314,10 @@ defmodule ToweropsWeb.DeviceLive.Form do
"Device created. "
end
if device.snmp_enabled do
_ = enqueue_discovery(device.id)
end
_ =
if device.snmp_enabled do
enqueue_discovery(device.id)
end
%{
text: base_message,
@ -540,11 +541,9 @@ defmodule ToweropsWeb.DeviceLive.Form do
end
end
@dev_env Mix.env() == :dev
defp skip_non_routable_check?(assigns) do
# Skip in dev mode or for specific users
@dev_env ||
Application.get_env(:towerops, :env) == :dev ||
(assigns[:current_scope] && assigns.current_scope.user &&
assigns.current_scope.user.email == "graham@mcintire.me")
end