prop/lib/microwaveprop/beacon_measurements.ex
Graham McIntire b4b8d4ec47
Some checks failed
Build base image / Build and push base image (push) Successful in 12s
Build and Push / Build CI test image (push) Successful in 14s
Build and Push / Build and Push Docker Image (push) Failing after 14m7s
simplify: DRY up shared changesets, context helpers, LiveView helpers, and structural extraction
- Create MaidenheadChangesetHelpers: consolidate grid validation, callsign
  normalization, lat/lon validation, grid/latlon derivation across 6 schemas
- Create ContextHelpers: shared fetch_owned with admin bypass, safe_enqueue
  for Oban workers, UUID casting to replace CastError rescues
- Extend LiveHelpers: add current_user/1 (removes 7 duplicate definitions),
  subscribe/2 (replaces 13 inline PubSub sites), assign_url_params/2
- Extract Propagation.ScoreStore (528 lines): separate file I/O and cache
  management from scoring logic, 13 defdelegate passthroughs
- Split SubmitLive (942->475 lines): extract CSV/ADIF upload rendering into
  3 function component modules (csv_upload, adif_upload, preview)
- Update 16 LiveViews to use shared helpers
2026-08-06 18:06:50 -05:00

105 lines
3.3 KiB
Elixir

defmodule Microwaveprop.BeaconMeasurements do
@moduledoc """
The BeaconMeasurements context: ingests beacon-signal-level
measurements uploaded by propmonitor clients.
See `Microwaveprop.BeaconMeasurements.BeaconMeasurement` for the
schema and field semantics.
"""
import Ecto.Query
alias Microwaveprop.BeaconMeasurements.BeaconMeasurement
alias Microwaveprop.BeaconMonitors.BeaconMonitor
alias Microwaveprop.Repo
@doc """
Inserts a measurement for the given monitor. `attrs` is expected to
carry the wire fields from `POST /api/v1/beacon-monitor/measurements`
(including `beacon_id` as a UUID string).
Returns `{:error, :beacon_not_found}` if the beacon UUID does not
resolve to an approved on-the-air beacon — the propmonitor client
drops the measurement on this, so we keep the differentiation from
generic 422s.
"""
@spec create_measurement(BeaconMonitor.t(), map()) ::
{:ok, BeaconMeasurement.t()}
| {:error, :beacon_not_found}
| {:error, Ecto.Changeset.t()}
def create_measurement(%BeaconMonitor{id: monitor_id}, attrs) when is_map(attrs) do
case lookup_beacon(attrs) do
:not_found ->
{:error, :beacon_not_found}
{:ok, beacon_id} ->
attrs =
attrs
|> normalize_attrs()
|> Map.put("monitor_id", monitor_id)
|> Map.put("beacon_id", beacon_id)
%BeaconMeasurement{}
|> BeaconMeasurement.changeset(attrs)
|> Repo.insert()
end
end
defp lookup_beacon(attrs) do
attrs = Map.new(attrs, fn {k, v} -> {to_string(k), v} end)
with id when is_binary(id) and byte_size(id) > 0 <- Map.get(attrs, "beacon_id"),
{:ok, uuid} <- Ecto.UUID.cast(id) do
# Only let monitors report for approved on-the-air beacons. The
# client drops 404s without retry, so an admin un-approving a
# beacon cleanly stops the monitor pointing at it.
query =
from b in Microwaveprop.Beacons.Beacon,
where: b.id == ^uuid and b.approved == true and b.on_the_air == true,
select: b.id
case Repo.one(query) do
nil -> :not_found
uuid -> {:ok, uuid}
end
else
_ -> :not_found
end
end
# The wire payload uses string keys (Phoenix JSON params). Make sure
# we don't drop a value because some intermediate handed us atom keys.
defp normalize_attrs(attrs) do
Map.new(attrs, fn
{k, v} when is_atom(k) -> {Atom.to_string(k), v}
{k, v} -> {k, v}
end)
end
@doc """
Returns the most recent measurements for the given beacon, newest first.
"""
@spec list_recent_for_beacon(Ecto.UUID.t(), pos_integer()) :: [BeaconMeasurement.t()]
def list_recent_for_beacon(beacon_id, limit \\ 100) do
Repo.all(
from m in BeaconMeasurement,
where: m.beacon_id == ^beacon_id,
order_by: [desc: m.measured_at],
limit: ^limit,
preload: [:monitor]
)
end
@doc """
Returns the most recent measurements for the given monitor, newest first.
"""
@spec list_recent_for_monitor(Ecto.UUID.t(), pos_integer()) :: [BeaconMeasurement.t()]
def list_recent_for_monitor(monitor_id, limit \\ 100) do
Repo.all(
from m in BeaconMeasurement,
where: m.monitor_id == ^monitor_id,
order_by: [desc: m.measured_at],
limit: ^limit
)
end
end