prop/lib/microwaveprop/beacon_monitors/beacon_monitor.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

123 lines
3.6 KiB
Elixir

defmodule Microwaveprop.BeaconMonitors.BeaconMonitor do
@moduledoc """
A beacon monitor is a physical SDR-based hardware unit assigned to a
user. The unit runs the `propmonitor` client which reports beacon
reception measurements back to Microwaveprop.
Each monitor has a unique random token the client uses to authenticate
its measurement uploads.
"""
use Ecto.Schema
import Ecto.Changeset
alias Microwaveprop.Accounts.User
alias Microwaveprop.Beacons.Beacon
alias Microwaveprop.Radio.MaidenheadChangesetHelpers, as: MCH
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id
schema "beacon_monitors" do
# Identity / label
field :name, :string
field :token, :string
field :last_seen_at, :utc_datetime
# Hardware identity
field :hardware_type, :string
field :hardware_id, :string
field :firmware_version, :string
# Antenna / installation
field :antenna_type, :string
field :antenna_gain_dbi, :float
field :lat, :float
field :lon, :float
field :callsign, :string
# Active configuration (admin + assigned user can change)
field :config_frequency_hz, :integer
field :config_integration_s, :integer
field :config_mode, :string
# Relationships
belongs_to :user, User
belongs_to :beacon, Beacon
belongs_to :assigned_by, User
timestamps(type: :utc_datetime)
end
@type t :: %__MODULE__{}
@doc """
General changeset — allows basic fields. Context-level functions
enforce which caller-role can set which fields.
"""
@spec changeset(t() | Ecto.Changeset.t(), map()) :: Ecto.Changeset.t()
def changeset(monitor, attrs) do
monitor
|> cast(attrs, [:name])
|> validate_required([:name])
|> validate_length(:name, min: 1, max: 100)
end
@doc """
Provisioning changeset — used by admins to register new hardware.
Casts all hardware-identity, antenna, and assignment fields.
"""
@spec provision_changeset(t() | Ecto.Changeset.t(), map()) :: Ecto.Changeset.t()
def provision_changeset(monitor, attrs) do
monitor
|> cast(attrs, [
:name,
:hardware_type,
:hardware_id,
:firmware_version,
:antenna_type,
:antenna_gain_dbi,
:lat,
:lon,
:callsign,
:user_id,
:assigned_by_id
])
|> validate_required([:name])
|> validate_length(:name, min: 1, max: 100)
|> validate_length(:hardware_type, max: 100)
|> validate_length(:hardware_id, max: 255)
|> validate_length(:firmware_version, max: 50)
|> validate_length(:antenna_type, max: 100)
|> validate_length(:callsign, max: 20)
|> validate_number(:antenna_gain_dbi, greater_than_or_equal_to: -20, less_than_or_equal_to: 60)
|> MCH.validate_latlon(:lat, :lon)
|> foreign_key_constraint(:user_id)
|> foreign_key_constraint(:assigned_by_id)
end
@doc """
Configuration changeset — used by admins and assigned users to update
what the monitor is listening for.
"""
@spec config_changeset(t() | Ecto.Changeset.t(), map()) :: Ecto.Changeset.t()
def config_changeset(monitor, attrs) do
monitor
|> cast(attrs, [
:name,
:beacon_id,
:callsign,
:lat,
:lon,
:config_frequency_hz,
:config_integration_s,
:config_mode
])
|> validate_required([:name])
|> validate_length(:name, min: 1, max: 100)
|> validate_length(:config_mode, max: 50)
|> validate_number(:config_frequency_hz, greater_than: 0)
|> validate_number(:config_integration_s, greater_than_or_equal_to: 5, less_than_or_equal_to: 3600)
|> foreign_key_constraint(:beacon_id)
end
end