Switch deployment pipeline from GitLab Agent kubectl to FluxCD image automation. CI now only builds and pushes tagged images to Forgejo registry; FluxCD detects new tags and updates the deployment manifest. Fix Ecto.MultipleResultsError on /dashboard when a device has multiple Preseem access point records by using limit(1) instead of Repo.get_by.
152 lines
4.2 KiB
Elixir
152 lines
4.2 KiB
Elixir
defmodule Towerops.Preseem do
|
|
@moduledoc """
|
|
Context for querying Preseem integration data.
|
|
"""
|
|
|
|
import Ecto.Query
|
|
|
|
alias Towerops.Preseem.AccessPoint
|
|
alias Towerops.Preseem.Insights
|
|
alias Towerops.Preseem.SubscriberMetric
|
|
alias Towerops.Repo
|
|
|
|
@doc "Get the Preseem AP linked to a specific device, or nil."
|
|
def get_access_point_for_device(device_id) do
|
|
AccessPoint
|
|
|> where(device_id: ^device_id)
|
|
|> limit(1)
|
|
|> Repo.one()
|
|
end
|
|
|
|
@doc "List all Preseem APs for an organization."
|
|
def list_access_points(organization_id) do
|
|
AccessPoint
|
|
|> where(organization_id: ^organization_id)
|
|
|> order_by(:name)
|
|
|> Repo.all()
|
|
end
|
|
|
|
@doc "List unmatched or ambiguous Preseem APs for an organization."
|
|
def list_unmatched_access_points(organization_id) do
|
|
AccessPoint
|
|
|> where(organization_id: ^organization_id)
|
|
|> where([ap], ap.match_confidence in ["unmatched", "ambiguous"])
|
|
|> order_by(:name)
|
|
|> Repo.all()
|
|
end
|
|
|
|
@doc "List matched Preseem APs for an organization (everything except unmatched/ambiguous)."
|
|
def list_matched_access_points(organization_id) do
|
|
AccessPoint
|
|
|> where(organization_id: ^organization_id)
|
|
|> where([ap], ap.match_confidence not in ["unmatched", "ambiguous"])
|
|
|> order_by(:name)
|
|
|> Repo.all()
|
|
end
|
|
|
|
@doc "List recent subscriber metrics for a Preseem AP. Default limit 100."
|
|
def list_subscriber_metrics(access_point_id, opts \\ []) do
|
|
limit = Keyword.get(opts, :limit, 100)
|
|
|
|
SubscriberMetric
|
|
|> where(preseem_access_point_id: ^access_point_id)
|
|
|> order_by(desc: :recorded_at)
|
|
|> limit(^limit)
|
|
|> Repo.all()
|
|
end
|
|
|
|
@doc "Link a Preseem AP to a device manually."
|
|
def link_access_point(access_point_id, device_id) do
|
|
case Repo.get(AccessPoint, access_point_id) do
|
|
nil ->
|
|
{:error, :not_found}
|
|
|
|
ap ->
|
|
ap
|
|
|> AccessPoint.changeset(%{device_id: device_id, match_confidence: "manual"})
|
|
|> Repo.update()
|
|
end
|
|
end
|
|
|
|
@doc "Unlink a Preseem AP from its device."
|
|
def unlink_access_point(access_point_id) do
|
|
case Repo.get(AccessPoint, access_point_id) do
|
|
nil ->
|
|
{:error, :not_found}
|
|
|
|
ap ->
|
|
ap
|
|
|> AccessPoint.changeset(%{device_id: nil, match_confidence: "unmatched"})
|
|
|> Repo.update()
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Returns an aggregate QoE summary for all APs at a site.
|
|
|
|
Returns a map with average QoE, latency, capacity info, and per-AP breakdowns.
|
|
"""
|
|
def get_site_qoe_summary(site_id) do
|
|
alias Towerops.Devices
|
|
|
|
device_ids =
|
|
site_id
|
|
|> Devices.list_site_devices()
|
|
|> Enum.map(& &1.id)
|
|
|
|
if device_ids == [] do
|
|
%{
|
|
avg_qoe: nil,
|
|
avg_capacity: nil,
|
|
total_subscribers: 0,
|
|
aps: [],
|
|
aps_with_issues: 0,
|
|
total_aps: 0
|
|
}
|
|
else
|
|
aps =
|
|
AccessPoint
|
|
|> where([ap], ap.device_id in ^device_ids)
|
|
|> Repo.all()
|
|
|
|
qoe_scores = aps |> Enum.map(& &1.qoe_score) |> Enum.reject(&is_nil/1)
|
|
capacity_scores = aps |> Enum.map(& &1.capacity_score) |> Enum.reject(&is_nil/1)
|
|
|
|
avg_qoe =
|
|
if qoe_scores == [],
|
|
do: nil,
|
|
else: Float.round(Enum.sum(qoe_scores) / length(qoe_scores), 1)
|
|
|
|
avg_capacity =
|
|
if capacity_scores == [],
|
|
do: nil,
|
|
else: Float.round(Enum.sum(capacity_scores) / length(capacity_scores), 1)
|
|
|
|
total_subscribers =
|
|
aps |> Enum.map(& &1.subscriber_count) |> Enum.reject(&is_nil/1) |> Enum.sum()
|
|
|
|
# APs with issues: QoE < 6.0 (on 1-10 scale) or capacity < 50
|
|
aps_with_issues =
|
|
Enum.count(aps, fn ap ->
|
|
(ap.qoe_score && ap.qoe_score < 6.0) ||
|
|
(ap.capacity_score && ap.capacity_score < 50.0)
|
|
end)
|
|
|
|
%{
|
|
avg_qoe: avg_qoe,
|
|
avg_capacity: avg_capacity,
|
|
total_subscribers: total_subscribers,
|
|
aps: aps,
|
|
aps_with_issues: aps_with_issues,
|
|
total_aps: length(aps)
|
|
}
|
|
end
|
|
end
|
|
|
|
# --- Insight delegations ---
|
|
|
|
defdelegate list_insights(organization_id, opts \\ []), to: Insights
|
|
defdelegate list_device_insights(device_id), to: Insights
|
|
defdelegate dismiss_insight(insight_id), to: Insights
|
|
defdelegate dismiss_insights(insight_ids), to: Insights
|
|
end
|