fix remaining bugs and quality improvements

Bug fixes:
- path_compute: use midpoint longitude (src+dest)/2 for time-of-day score
- rate_limiter: guard nil token id from burning auth rate limit bucket

Robustness:
- scorer: replace fragile MatchError-prone pattern match with per-key
  fallback — each invariant computed individually if absent
- telemetry: log exception in rescue instead of silent swallow
- grid: add atom-key clause to contains?() consistent with point_source/1
- grid: round float_range values to avoid fp drift in snapped lookups

Cleanup:
- path_compute: remove unused alias Geo (import-only module)
This commit is contained in:
Graham McIntire 2026-05-24 11:56:17 -05:00
parent e3d430f8c4
commit 4d3b61c740
No known key found for this signature in database
GPG key ID: F4ABF488E6029E59
7 changed files with 31 additions and 20 deletions

View file

@ -60,6 +60,10 @@ defmodule Microwaveprop.Propagation.Grid do
lat >= @lat_min and lat <= @lat_max and lon >= @lon_min and lon <= @lon_max
end
def contains?(%{lat: lat, lon: lon}) when is_number(lat) and is_number(lon) do
lat >= @lat_min and lat <= @lat_max and lon >= @lon_min and lon <= @lon_max
end
def contains?(_), do: false
@doc "Returns the grid specification for wgrib2 -lola extraction."
@ -133,6 +137,9 @@ defmodule Microwaveprop.Propagation.Grid do
defp float_range(start, stop, step) do
count = round((stop - start) / step) + 1
Enum.map(0..(count - 1), fn i -> start + i * step end)
# Avoid floating-point drift by rounding each value to 3 decimals.
# The accumulation of start + i * step drifts beyond 0.001° after ~200
# iterations, which can cause map lookups on snapped coordinates to miss.
Enum.map(0..(count - 1), fn i -> Float.round(start + i * step, 3) end)
end
end

View file

@ -16,7 +16,6 @@ defmodule Microwaveprop.Propagation.PathCompute do
import Microwaveprop.Geo, only: [haversine_km: 4, bearing_deg: 4]
alias Microwaveprop.Geo
alias Microwaveprop.Ionosphere
alias Microwaveprop.Propagation
alias Microwaveprop.Propagation.BandConfig
@ -405,7 +404,7 @@ defmodule Microwaveprop.Propagation.PathCompute do
utc_minute: now.minute,
month: now.month,
latitude: (src.lat + dst.lat) / 2,
longitude: src.lon,
longitude: (src.lon + dst.lon) / 2,
pressure_mb: if(pressures != [], do: Enum.min(pressures)),
prev_pressure_mb: nil,
rain_rate_mmhr: 0.0,
@ -485,6 +484,5 @@ defmodule Microwaveprop.Propagation.PathCompute do
}
end
# Avoid unused-alias warnings for the import.
@compile {:no_warn_undefined, Geo}
end

View file

@ -553,17 +553,10 @@ defmodule Microwaveprop.Propagation.Scorer do
# scorer for per-point reuse across bands) or all absent. We check
# one key and derive the rest from the same branch so we never
# silently mix cached and freshly-computed values.
%{
tod_score: tod_score,
sky_score: sky_score,
wind_score: wind_score,
pressure_score: pressure_score
} =
if Map.has_key?(conditions, :tod_score) do
conditions
else
precompute_band_invariants(conditions)
end
tod_score = conditions[:tod_score] || elem(score_time_of_day(conditions.utc_hour, conditions.utc_minute, conditions.month, conditions.longitude), 0)
sky_score = conditions[:sky_score] || score_sky(conditions.sky_cover_pct)
wind_score = conditions[:wind_score] || score_wind(conditions.wind_speed_kts)
pressure_score = conditions[:pressure_score] || score_pressure(conditions.pressure_mb, conditions.prev_pressure_mb)
factors = %{
humidity: score_humidity(conditions.abs_humidity, band_config),

View file

@ -116,7 +116,7 @@ defmodule Microwaveprop.Radio.Contact do
|> validate_callsign(:station2)
|> validate_grid_format(:grid1)
|> validate_grid_format(:grid2)
|> validate_inclusion(:mode, @allowed_modes)
|> validate_mode_inclusion()
|> validate_inclusion(:band, @allowed_bands)
|> validate_email_format()
|> validate_length(:submitter_email, max: 254)
@ -168,6 +168,16 @@ defmodule Microwaveprop.Radio.Contact do
end
end
# Only run validate_inclusion for mode when the field has a non-nil change.
# nil means "not provided" (mode is optional), and put_change to nil would
# otherwise cause validate_inclusion to reject nil as not in the allowed list.
defp validate_mode_inclusion(changeset) do
case get_change(changeset, :mode) do
nil -> changeset
mode -> validate_inclusion(changeset, :mode, @allowed_modes)
end
end
# Either a user_id (logged-in submitter) or a submitter_email (anonymous)
# must be present so we know who submitted the contact.
defp validate_user_or_email(changeset) do

View file

@ -101,7 +101,7 @@ defmodule MicrowavepropWeb.Api.RateLimiter do
defp bucket_for(conn, opts) do
case conn.assigns[:current_api_token] do
%{id: id} ->
%{id: id} when is_binary(id) ->
{{:token, id}, Keyword.get(opts, :auth_limit, @default_auth_limit)}
_ ->

View file

@ -282,6 +282,9 @@ defmodule MicrowavepropWeb.Telemetry do
:ok
rescue
# Don't crash the poller if the DB blips — telemetry is best-effort.
_ -> :ok
e ->
require Logger
Logger.warning("Oban queue depth poll failed: #{inspect(e)}")
:ok
end
end

View file

@ -161,7 +161,7 @@ defmodule MicrowavepropWeb.UserAuth do
# Do not renew session if the user is already logged in
# to prevent CSRF errors or data being lost in tabs that are still open
defp renew_session(conn, user) when conn.assigns.current_scope.user.id == user.id do
defp renew_session(conn, %{id: user_id}) when conn.assigns.current_scope.user.id == user_id do
conn
end