P0 (security-critical): - Gate CSV/ADIF upload tabs behind authentication, add 30s cooldown to all upload handlers - Cap CSV/ADIF imports at 2,000 rows server-side in both parsers - Add submitter_verified boolean to contacts (client-cannot-set, anonymous=false) - Create k8s/secret.example.yaml with placeholders, add LIVE_VIEW_SIGNING_SALT P1 (high-priority): - Add Mox.verify_on_exit!() to valkey_test.exs - Replace DateTime.utc_now() truncation with static ~U literals in map_live_test.exs - Replace Process.sleep with render_async in pskr_spots_live_test.exs (6 occurrences) - Add MonitorLive.Show test coverage (4 tests: owner view, non-owner redirect, config success/error) - Extract duct-detection and mechanism-classification logic from ContactLive.Show into Propagation.PathAnalysis - Split ContactLive.Show render into 12 function components - Update CLAUDE.md: remove stale ML model, mark HRDPS active, add backtest/pskr dirs - Batch CSV import enrichment jobs via new enqueue_for_contacts/1 P2 (medium-priority): - Set secure:true on session and remember-me cookies in production - Change SMTP TLS from verify_none to verify_peer with public_key cacerts - Make /metrics fail-closed in production when PROMETHEUS_AUTH_TOKEN unset - Add RateLimiter (anon_limit:10, auth_limit:60) to /api/contacts/map - Add content-security-policy-report-only header - Add comment noting String.to_atom is compile-time safe in hrdps_client.ex - Delegate duplicated haversine_km to canonical Microwaveprop.Geo.haversine_km/4 - Consolidate score-tier/color/verdict formatting into Microwaveprop.Format - Update CLAUDE.md testing section to match actual raw-string-matching practice - Batch HrrrPointEnqueuer Repo.insert_all calls to single round-trip - Split weather.ex (1696→216 lines) and radio.ex (1285→54 lines) into purpose-based sub-facades P3 (low-priority): - Add LIVE_VIEW_SIGNING_SALT warning comment, extend filter_parameters - Add host/community validation to snmp_client.ex - Add raw/1 safety comment in algo_live.ex - Add hex-audit and cargo-audit Makefile targets - Add privacy_live smoke test - Replace notify_listener busy-poll loop with Process.monitor/1 + assert_receive - Add ContactCommonVolumeRadar changeset validation tests (5 tests)
73 lines
2.3 KiB
Elixir
73 lines
2.3 KiB
Elixir
defmodule Microwaveprop.Weather.Iemre do
|
||
@moduledoc false
|
||
|
||
import Ecto.Query
|
||
|
||
alias Microwaveprop.Radio
|
||
alias Microwaveprop.Radio.Contact
|
||
alias Microwaveprop.Repo
|
||
alias Microwaveprop.Weather.IemreObservation
|
||
alias Microwaveprop.Weather.ProfileLookup
|
||
|
||
# Delegates to ProfileLookup
|
||
defdelegate iemre_for_contact(contact), to: ProfileLookup
|
||
defdelegate iemre_for_path(contact), to: ProfileLookup
|
||
defdelegate find_nearest_iemre(lat, lon, timestamp), to: ProfileLookup
|
||
defdelegate round_to_iemre_grid(lat, lon), to: ProfileLookup
|
||
|
||
@spec upsert_iemre_observation(map()) :: {:ok, IemreObservation.t()} | {:error, Ecto.Changeset.t()}
|
||
def upsert_iemre_observation(attrs) do
|
||
%IemreObservation{}
|
||
|> IemreObservation.changeset(attrs)
|
||
|> Repo.insert(
|
||
on_conflict: :nothing,
|
||
conflict_target: [:lat, :lon, :date]
|
||
)
|
||
end
|
||
|
||
@spec has_iemre_observation?(float(), float(), Date.t()) :: boolean()
|
||
def has_iemre_observation?(lat, lon, date) do
|
||
IemreObservation
|
||
|> where([i], i.lat == ^lat and i.lon == ^lon and i.date == ^date)
|
||
|> Repo.exists?()
|
||
end
|
||
|
||
@doc """
|
||
Flip `contacts.iemre_status` from `:queued` to `:complete` for every
|
||
contact whose path points all have an `iemre_observations` row at
|
||
the rounded grid cell for the QSO's UTC date.
|
||
|
||
Per-contact check (via the existing helpers); kept in Elixir because
|
||
`contact_path_points/1` produces 1–3 points depending on whether
|
||
`pos2` is set, and rounding to the 0.125° IEMRE grid per point in
|
||
pure SQL is awkward. Queue sizes are small (<20 stuck at steady
|
||
state).
|
||
"""
|
||
@spec reconcile_iemre_statuses() :: {:ok, non_neg_integer()}
|
||
def reconcile_iemre_statuses do
|
||
queued =
|
||
Contact
|
||
|> where([c], c.iemre_status == :queued and not is_nil(c.pos1))
|
||
|> Repo.all()
|
||
|
||
to_complete =
|
||
Enum.filter(queued, fn c ->
|
||
date = DateTime.to_date(c.qso_timestamp)
|
||
|
||
c
|
||
|> Radio.contact_path_points()
|
||
|> Enum.all?(fn {lat, lon} ->
|
||
{rlat, rlon} = round_to_iemre_grid(lat, lon)
|
||
has_iemre_observation?(rlat, rlon, date)
|
||
end)
|
||
end)
|
||
|
||
if to_complete == [] do
|
||
{:ok, 0}
|
||
else
|
||
ids = Enum.map(to_complete, & &1.id)
|
||
_ = Radio.set_enrichment_status!(ids, :iemre_status, :complete)
|
||
{:ok, length(to_complete)}
|
||
end
|
||
end
|
||
end
|