- 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
138 lines
3.9 KiB
Elixir
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
|