towerops/lib/towerops/alerts.ex
Graham McIntire 155b38ab94
feat: add APC, Arista, Calix, ADTRAN, Allied Telesis vendor modules
Add 5 new vendor modules for network infrastructure equipment:
- APC: UPS and PDU monitoring (battery, voltage, load, runtime)
- Arista EOS: Data center switches (CPU, memory)
- Calix: GPON/fiber access equipment
- ADTRAN AOS: NetVanta routers and switches
- Allied Telesis: AlliedWare/AlliedWare Plus switches

Total: 53 vendors, 88 profile names

Also cleanup commented AlertNotifier code from previous removal.
2026-01-22 11:00:21 -06:00

172 lines
4.3 KiB
Elixir

defmodule Towerops.Alerts do
@moduledoc """
The Alerts context.
"""
import Ecto.Query
alias Towerops.Alerts.Alert
alias Towerops.Repo
@doc """
Creates an alert for device status change.
"""
def create_alert(attrs) do
%Alert{}
|> Alert.changeset(attrs)
|> Repo.insert()
end
@doc """
Returns the list of alerts for an device.
"""
def list_devices_alerts(device_id, limit \\ 100) do
Repo.all(
from(a in Alert,
where: a.device_id == ^device_id,
order_by: [desc: a.triggered_at],
limit: ^limit,
preload: [:device, :acknowledged_by]
)
)
end
@doc """
Returns the list of active (unresolved) alerts for an organization.
Only returns equipment_down alerts, as equipment_up alerts are informational.
"""
def list_organization_active_alerts(organization_id) do
Repo.all(
from(a in Alert,
join: e in assoc(a, :device),
join: s in assoc(e, :site),
where: s.organization_id == ^organization_id,
where: a.alert_type == :device_down,
where: is_nil(a.resolved_at),
order_by: [desc: a.triggered_at],
preload: [:acknowledged_by, device: {e, site: s}]
)
)
end
@doc """
Returns the count of active (unresolved) alerts for an organization.
"""
def count_active_alerts(organization_id) do
Repo.aggregate(
from(a in Alert,
join: e in assoc(a, :device),
join: s in assoc(e, :site),
where: s.organization_id == ^organization_id,
where: a.alert_type == :device_down,
where: is_nil(a.resolved_at)
),
:count
)
end
@doc """
Returns the list of all alerts for an organization.
Accepts either an integer limit or a map of filters:
- limit (integer): Max number of alerts to return
- filters (map):
- status: Filter by status ("active", "acknowledged", "resolved")
- limit: Max number of alerts to return
"""
def list_organization_alerts(organization_id, limit) when is_integer(limit) do
list_organization_alerts(organization_id, %{"limit" => limit})
end
def list_organization_alerts(organization_id, filters) when is_map(filters) do
limit = filters["limit"] || 100
query =
from(a in Alert,
join: e in assoc(a, :device),
join: s in assoc(e, :site),
where: s.organization_id == ^organization_id,
order_by: [desc: a.triggered_at],
limit: ^limit,
preload: [device: {e, site: s}, acknowledged_by: []]
)
query =
case filters["status"] do
"active" -> where(query, [a], is_nil(a.resolved_at) and is_nil(a.acknowledged_at))
"acknowledged" -> where(query, [a], is_nil(a.resolved_at) and not is_nil(a.acknowledged_at))
"resolved" -> where(query, [a], not is_nil(a.resolved_at))
_ -> query
end
Repo.all(query)
end
@doc """
Gets a single alert.
where: s.organization_id == ^organization_id,
order_by: [desc: a.triggered_at],
limit: ^limit,
preload: [device: {e, site: s}, acknowledged_by: []]
)
)
end
@doc \"""
Gets a single alert.
"""
def get_alert!(id) do
Alert
|> Repo.get!(id)
|> Repo.preload([:device, :acknowledged_by])
end
@doc """
Acknowledges an alert.
"""
def acknowledge_alert(alert, user_id) do
alert
|> Alert.changeset(%{
acknowledged_at: DateTime.truncate(DateTime.utc_now(), :second),
acknowledged_by_id: user_id
})
|> Repo.update()
end
@doc """
Resolves an alert.
"""
def resolve_alert(alert) do
alert
|> Alert.changeset(%{resolved_at: DateTime.truncate(DateTime.utc_now(), :second)})
|> Repo.update()
end
@doc """
Checks if there's an active alert of the same type for the device.
"""
def has_active_alert?(device_id, alert_type) do
Repo.exists?(
from(a in Alert,
where: a.device_id == ^device_id,
where: a.alert_type == ^alert_type,
where: is_nil(a.resolved_at)
)
)
end
@doc """
Gets the latest active alert of a specific type for device.
"""
def get_active_alert(device_id, alert_type) do
Repo.one(
from(a in Alert,
where: a.device_id == ^device_id,
where: a.alert_type == ^alert_type,
where: is_nil(a.resolved_at),
order_by: [desc: a.triggered_at],
limit: 1
)
)
end
end