towerops/lib/towerops/rf_links.ex
Graham McIntire 7c8731f86a fix: make sidebar sticky with header and resolve all credo issues (#32)
- Changed sidebar from fixed to sticky positioning
- Wrapped sidebar and main content in flex container
- Sidebar now sticks to top along with header when scrolling
- Removed unused Ecto.Query import from gps_sync.ex

Credo improvements (17 → 0 remaining):
- Replaced all length/1 checks with empty list comparisons
- Extracted helper functions to reduce nesting depth
- Split complex functions into smaller, testable units
- Applied 'with' statements for clearer control flow
- Refactored high-complexity functions (cyclomatic complexity)
- Files improved: storm_detector, alert_notification_worker,
  alert_digest_worker, gps_sync, statistics_sync, device_sync,
  site_sync, site_correlation, reports_live, topology,
  pagerduty/client, cn_maestro/sync

Reviewed-on: graham/towerops-web#32
2026-03-15 18:19:09 -05:00

138 lines
3.9 KiB
Elixir

defmodule Towerops.RfLinks do
@moduledoc """
Context module for RF link health data.
Aggregates wireless client data with signal strength, SNR, and capacity
metrics for NOC-focused triage views.
"""
import Ecto.Query
alias Towerops.Repo
alias Towerops.Snmp.WirelessClient
alias Towerops.Snmp.WirelessClientReading
@doc """
Returns all wireless clients (RF links) for an organization with their
latest readings and health classification.
Results are sorted by signal strength ascending (weakest first).
"""
def list_rf_links(organization_id, opts \\ []) do
filter = Keyword.get(opts, :filter, :all)
WirelessClient
|> where(organization_id: ^organization_id)
|> preload(:device)
|> Repo.all()
|> Enum.map(&enrich_link/1)
|> maybe_filter(filter)
|> Enum.sort_by(& &1.signal_strength, fn
nil, nil -> true
nil, _ -> true
_, nil -> false
a, b -> a <= b
end)
end
@doc """
Returns summary counts for stat cards.
"""
def get_summary(organization_id) do
links = list_rf_links(organization_id)
total = length(links)
healthy = Enum.count(links, &(&1.health == :healthy))
degraded = Enum.count(links, &(&1.health == :degraded))
critical = Enum.count(links, &(&1.health == :critical))
%{
total: total,
healthy: healthy,
degraded: degraded,
critical: critical
}
end
@doc """
Fetches the last 24h of readings for a specific wireless client,
used for sparkline rendering.
"""
def get_readings_24h(wireless_client_id) do
cutoff = DateTime.add(DateTime.utc_now(), -24, :hour)
WirelessClientReading
|> where(wireless_client_id: ^wireless_client_id)
|> where([r], r.checked_at >= ^cutoff)
|> order_by(asc: :checked_at)
|> select([r], %{
checked_at: r.checked_at,
signal_strength: r.signal_strength,
snr: r.snr,
tx_rate: r.tx_rate,
rx_rate: r.rx_rate
})
|> Repo.all()
end
# Health classification based on issue spec thresholds
defp classify_health(signal, snr) do
cond do
is_nil(signal) and is_nil(snr) -> :unknown
signal_ok?(signal, -65) and snr_ok?(snr, 25) -> :healthy
signal_ok?(signal, -75) and snr_ok?(snr, 15) -> :degraded
true -> :critical
end
end
defp signal_ok?(nil, _threshold), do: true
defp signal_ok?(signal, threshold), do: signal > threshold
defp snr_ok?(nil, _threshold), do: true
defp snr_ok?(snr, threshold), do: snr > threshold
defp enrich_link(%WirelessClient{} = client) do
capacity_utilization = calculate_capacity_utilization(client)
%{
id: client.id,
device_id: client.device_id,
device: client.device,
mac_address: client.mac_address,
ip_address: client.ip_address,
hostname: client.hostname,
signal_strength: client.signal_strength,
snr: client.snr,
distance: client.distance,
tx_rate: client.tx_rate,
rx_rate: client.rx_rate,
uptime_seconds: client.uptime_seconds,
last_seen_at: client.last_seen_at,
health: classify_health(client.signal_strength, client.snr),
capacity_utilization: capacity_utilization,
metadata: client.metadata
}
end
defp calculate_capacity_utilization(%{tx_rate: tx, rx_rate: rx, metadata: metadata}) do
max_rate = get_in(metadata || %{}, ["max_rate"])
cond do
is_nil(max_rate) or max_rate == 0 ->
nil
is_nil(tx) and is_nil(rx) ->
nil
true ->
current = max(tx || 0, rx || 0)
Float.round(current / max_rate * 100, 1)
end
end
defp maybe_filter(links, :all), do: links
defp maybe_filter(links, :healthy), do: Enum.filter(links, &(&1.health == :healthy))
defp maybe_filter(links, :degraded), do: Enum.filter(links, &(&1.health == :degraded))
defp maybe_filter(links, :critical), do: Enum.filter(links, &(&1.health == :critical))
defp maybe_filter(links, _), do: links
end