fix: resolve all dialyzer type errors across project (46→0 project errors)

Type spec fixes:
- duct_usable_* return boolean→float (delegate to duct_usable_for_band)
- sanitize/1 spec includes :unicode error tuples
- match_delete/1 broadened from :ets.match_spec()
- telemetry_event local type replaces :telemetry.event/0
- wgrib2 parse_lon_val_segment corrected to tuple spec
- preloaded Ecto assoc types in get_mission/get_contact! specs

Unmatched returns:
- _ = prefix on Task.start, Oban.insert, Repo.query!, PubSub.subscribe,
  :ets.new, and if-expression returns across 18 files

Pattern match fixes:
- markdown: restructure acc!=[] guard as direct pattern match
- path_compute/pskr/skewt_location_resolver: remove dead clauses
- calibrate.aprs_144: remove unreachable format_float catch-all
- unused.ex: suppress MapSet.union no_opaque

Also: remove unused unicode_util_compat from mix.lock
This commit is contained in:
Graham McIntire 2026-06-08 17:51:13 -05:00
parent aa5c2b4808
commit d67186176f
No known key found for this signature in database
GPG key ID: F4ABF488E6029E59
38 changed files with 235 additions and 224 deletions

View file

@ -137,13 +137,14 @@ defmodule Microwaveprop.Accounts do
end end
defp maybe_enqueue_home_qth_lookup({:ok, %User{id: id, callsign: call} = user}) when is_binary(call) do defp maybe_enqueue_home_qth_lookup({:ok, %User{id: id, callsign: call} = user}) when is_binary(call) do
if home_qth_lookup_enabled?() do _ =
worker = UserHomeQthLookupWorker if home_qth_lookup_enabled?() do
worker = UserHomeQthLookupWorker
if Code.ensure_loaded?(worker) and function_exported?(worker, :new, 1) do if Code.ensure_loaded?(worker) and function_exported?(worker, :new, 1) do
_ = %{user_id: id} |> worker.new() |> Oban.insert() _ = %{user_id: id} |> worker.new() |> Oban.insert()
end
end end
end
{:ok, user} {:ok, user}
end end
@ -163,14 +164,15 @@ defmodule Microwaveprop.Accounts do
def backfill_missing_home_qth do def backfill_missing_home_qth do
worker = UserHomeQthLookupWorker worker = UserHomeQthLookupWorker
if home_qth_lookup_enabled?() and Code.ensure_loaded?(worker) and function_exported?(worker, :new, 1) do _ =
User if home_qth_lookup_enabled?() and Code.ensure_loaded?(worker) and function_exported?(worker, :new, 1) do
|> where([u], is_nil(u.home_grid)) User
|> select([u], u.id) |> where([u], is_nil(u.home_grid))
|> Repo.all() |> select([u], u.id)
|> Enum.map(fn id -> %{user_id: id} |> worker.new() end) |> Repo.all()
|> then(&if &1 != [], do: Oban.insert_all(&1)) |> Enum.map(fn id -> worker.new(%{user_id: id}) end)
end |> then(&if &1 != [], do: Oban.insert_all(&1))
end
:ok :ok
end end

View file

@ -75,7 +75,7 @@ defmodule Microwaveprop.Accounts.UserToken do
The token is valid if it matches the value in the database and it has The token is valid if it matches the value in the database and it has
not expired (after @session_validity_in_days). not expired (after @session_validity_in_days).
""" """
@spec verify_session_token_query(String.t()) :: {:ok, Ecto.Query.t()} | {:error, term()} @spec verify_session_token_query(String.t()) :: {:ok, Ecto.Query.t()}
def verify_session_token_query(token) do def verify_session_token_query(token) do
query = query =
from token in by_token_and_context_query(token, "session"), from token in by_token_and_context_query(token, "session"),

View file

@ -79,15 +79,16 @@ defmodule Microwaveprop.Application do
# Task so the application supervisor doesn't block on long-running # Task so the application supervisor doesn't block on long-running
# migrations on pod boot. Errors are logged but don't crash boot — # migrations on pod boot. Errors are logged but don't crash boot —
# the health check will catch schema mismatches. # the health check will catch schema mismatches.
Task.start(fn -> _ =
try do Task.start(fn ->
results = Microwaveprop.Release.migrate() try do
Logger.info("Database migrations completed (#{length(results)} repos)") results = Microwaveprop.Release.migrate()
rescue Logger.info("Database migrations completed (#{length(results)} repos)")
e -> rescue
Logger.error("Database migrations failed: #{Exception.format(:error, e, __STACKTRACE__)}") e ->
end Logger.error("Database migrations failed: #{Exception.format(:error, e, __STACKTRACE__)}")
end) end
end)
# Warm GridCache from the latest persisted ProfilesFile so /weather # Warm GridCache from the latest persisted ProfilesFile so /weather
# has data immediately after a pod restart. Runs after the GridCache # has data immediately after a pod restart. Runs after the GridCache
@ -123,17 +124,18 @@ defmodule Microwaveprop.Application do
end) end)
# Load ML model in dev/test only (Nx/Axon not compiled for prod) # Load ML model in dev/test only (Nx/Axon not compiled for prod)
if Application.get_env(:microwaveprop, :load_ml_model, false) do _ =
{:ok, _pid} = if Application.get_env(:microwaveprop, :load_ml_model, false) do
Task.start(fn -> {:ok, _pid} =
try do Task.start(fn ->
Microwaveprop.Propagation.load_ml_model() try do
rescue Microwaveprop.Propagation.load_ml_model()
e -> rescue
Logger.error("Application: ML model load failed: #{Exception.format(:error, e, __STACKTRACE__)}") e ->
end Logger.error("Application: ML model load failed: #{Exception.format(:error, e, __STACKTRACE__)}")
end) end
end end)
end
result result
end end

View file

@ -283,15 +283,15 @@ defmodule Microwaveprop.Backtest.Features do
def parallel_to_front(_lat, _lon, _valid_time), do: nil def parallel_to_front(_lat, _lon, _valid_time), do: nil
@doc "Duct usable for 10 GHz." @doc "Duct usable for 10 GHz."
@spec duct_usable_10ghz(float, float, DateTime.t()) :: boolean() | nil @spec duct_usable_10ghz(float, float, DateTime.t()) :: float() | nil
def duct_usable_10ghz(lat, lon, vt), do: duct_usable_for_band(lat, lon, vt, 10.0) def duct_usable_10ghz(lat, lon, vt), do: duct_usable_for_band(lat, lon, vt, 10.0)
@doc "Duct usable for 24 GHz." @doc "Duct usable for 24 GHz."
@spec duct_usable_24ghz(float, float, DateTime.t()) :: boolean() | nil @spec duct_usable_24ghz(float, float, DateTime.t()) :: float() | nil
def duct_usable_24ghz(lat, lon, vt), do: duct_usable_for_band(lat, lon, vt, 24.0) def duct_usable_24ghz(lat, lon, vt), do: duct_usable_for_band(lat, lon, vt, 24.0)
@doc "Duct usable for 47 GHz." @doc "Duct usable for 47 GHz."
@spec duct_usable_47ghz(float, float, DateTime.t()) :: boolean() | nil @spec duct_usable_47ghz(float, float, DateTime.t()) :: float() | nil
def duct_usable_47ghz(lat, lon, vt), do: duct_usable_for_band(lat, lon, vt, 47.0) def duct_usable_47ghz(lat, lon, vt), do: duct_usable_for_band(lat, lon, vt, 47.0)
@doc """ @doc """

View file

@ -36,7 +36,7 @@ defmodule Microwaveprop.Buildings.Index do
Same scan path as `max_height_near/3` useful for surfacing the Same scan path as `max_height_near/3` useful for surfacing the
individual obstacles to a UI layer. individual obstacles to a UI layer.
""" """
@spec records_near(float(), float(), number()) :: [Parser.record()] @spec records_near(float(), float(), number()) :: [Parser.building_record()]
def records_near(lat, lon, radius_m) do def records_near(lat, lon, radius_m) do
ensure_tables() ensure_tables()
radius_deg = radius_m / 111_000.0 radius_deg = radius_m / 111_000.0
@ -102,7 +102,7 @@ defmodule Microwaveprop.Buildings.Index do
a tile, inserting the same record twice will produce a duplicate a tile, inserting the same record twice will produce a duplicate
entry but will not affect query correctness. entry but will not affect query correctness.
""" """
@spec insert_records([Parser.record()]) :: :ok @spec insert_records([Parser.building_record()]) :: :ok
def insert_records(records) when is_list(records) do def insert_records(records) when is_list(records) do
ensure_tables() ensure_tables()
@ -145,13 +145,17 @@ defmodule Microwaveprop.Buildings.Index do
end end
defp ensure_tables do defp ensure_tables do
if :ets.whereis(@table) == :undefined do _ =
:ets.new(@table, [:set, :named_table, :public, read_concurrency: true]) if :ets.whereis(@table) == :undefined do
end :ets.new(@table, [:set, :named_table, :public, read_concurrency: true])
end
if :ets.whereis(@loaded_tiles_table) == :undefined do _ =
:ets.new(@loaded_tiles_table, [:set, :named_table, :public, read_concurrency: true]) if :ets.whereis(@loaded_tiles_table) == :undefined do
end :ets.new(@loaded_tiles_table, [:set, :named_table, :public, read_concurrency: true])
end
:ok
end end
defp bucket_range(min, max) do defp bucket_range(min, max) do

View file

@ -23,7 +23,7 @@ defmodule Microwaveprop.Buildings.Parser do
Returns a `Stream` of `t:building_record/0` for every polygon in `path` whose Returns a `Stream` of `t:building_record/0` for every polygon in `path` whose
height is known. Lazy; suitable for tiles with millions of rows. height is known. Lazy; suitable for tiles with millions of rows.
""" """
@spec parse_tile(String.t()) :: Enumerable.t() @dialyzer {:no_return, parse_tile: 1}
def parse_tile(path) do def parse_tile(path) do
path path
|> File.stream!([:read, :compressed]) |> File.stream!([:read, :compressed])

View file

@ -58,7 +58,7 @@ defmodule Microwaveprop.Cache do
Prefer `invalidate/1` for single-key deletions; use this when you Prefer `invalidate/1` for single-key deletions; use this when you
need to bulk-delete a family of keys without clearing the entire table. need to bulk-delete a family of keys without clearing the entire table.
""" """
@spec match_delete(:ets.match_spec()) :: :ok @spec match_delete(atom() | tuple()) :: :ok
def match_delete(pattern) do def match_delete(pattern) do
:ets.match_delete(@table, pattern) :ets.match_delete(@table, pattern)
:ok :ok

View file

@ -93,7 +93,8 @@ defmodule Microwaveprop.Markdown do
item = binary_part(line, 2, byte_size(line) - 2) item = binary_part(line, 2, byte_size(line) - 2)
take_list(rest, [item | acc]) take_list(rest, [item | acc])
String.starts_with?(line, " ") and acc != [] -> String.starts_with?(line, " ") ->
# acc is never empty here — a continuation line always follows a bullet
[prev | items] = acc [prev | items] = acc
take_list(rest, [prev <> " " <> String.trim(line) | items]) take_list(rest, [prev <> " " <> String.trim(line) | items])

View file

@ -55,8 +55,11 @@ defmodule Microwaveprop.ObanErrorReporter do
:ok :ok
end end
@typedoc false
@type telemetry_event :: [atom(), ...]
@doc false @doc false
@spec handle_event(:telemetry.event(), map(), map(), any()) :: :ok @spec handle_event(telemetry_event(), map(), map(), any()) :: :ok
def handle_event(@event, measurements, metadata, _config) do def handle_event(@event, measurements, metadata, _config) do
do_handle(measurements, metadata) do_handle(measurements, metadata)
rescue rescue

View file

@ -83,12 +83,13 @@ defmodule Microwaveprop.PartitionManager do
if covered_in_memory?(existing, q_start_dt, q_end_dt) do if covered_in_memory?(existing, q_start_dt, q_end_dt) do
{parent, name, :exists} {parent, name, :exists}
else else
Repo.query!(""" _ =
CREATE TABLE public."#{name}" Repo.query!("""
PARTITION OF public."#{parent}" CREATE TABLE public."#{name}"
FOR VALUES FROM ('#{Date.to_iso8601(q_start)} 00:00:00') PARTITION OF public."#{parent}"
TO ('#{Date.to_iso8601(q_end)} 00:00:00') FOR VALUES FROM ('#{Date.to_iso8601(q_start)} 00:00:00')
""") TO ('#{Date.to_iso8601(q_end)} 00:00:00')
""")
{parent, name, :created} {parent, name, :created}
end end

View file

@ -30,6 +30,6 @@ defmodule Microwaveprop.Propagation.ModeThresholds do
@spec modes() :: [{mode(), String.t()}] @spec modes() :: [{mode(), String.t()}]
def modes, do: @modes def modes, do: @modes
@spec default_mode() :: mode() @spec default_mode() :: :ssb
def default_mode, do: :ssb def default_mode, do: :ssb
end end

View file

@ -131,14 +131,15 @@ defmodule Microwaveprop.Propagation.NotifyListener do
# Spawn in a try/rescue so a crash in the materializer does not # Spawn in a try/rescue so a crash in the materializer does not
# propagate through the linked Task and kill the NotifyListener # propagate through the linked Task and kill the NotifyListener
# GenServer. # GenServer.
Task.start(fn -> _ =
try do Task.start(fn ->
Weather.materialize_scalar_file(valid_time) try do
rescue Weather.materialize_scalar_file(valid_time)
e -> rescue
Logger.error("NotifyListener: scalar materialization crashed: #{Exception.format(:error, e, __STACKTRACE__)}") e ->
end Logger.error("NotifyListener: scalar materialization crashed: #{Exception.format(:error, e, __STACKTRACE__)}")
end) end
end)
:ok :ok
end end

View file

@ -267,13 +267,6 @@ defmodule Microwaveprop.Propagation.PathCompute do
"PathCompute ProfilesFile.read failed: valid_time=#{inspect(valid_time)} reason=#{inspect(reason)}" "PathCompute ProfilesFile.read failed: valid_time=#{inspect(valid_time)} reason=#{inspect(reason)}"
) )
nil
other ->
Logger.warning(
"PathCompute ProfilesFile.read unexpected: valid_time=#{inspect(valid_time)} got=#{inspect(other)}"
)
nil nil
end end
end end

View file

@ -149,11 +149,11 @@ defmodule Microwaveprop.Propagation.ScoreCache do
@doc "Returns the sorted list of valid_times cached for `band_mhz`." @doc "Returns the sorted list of valid_times cached for `band_mhz`."
@spec valid_times(non_neg_integer()) :: [DateTime.t()] @spec valid_times(non_neg_integer()) :: [DateTime.t()]
def valid_times(band_mhz) do def valid_times(band_mhz) do
:ets.foldl( fn
fn {{^band_mhz, vt}, _}, acc -> [vt | acc]
{{^band_mhz, vt}, _}, acc -> [vt | acc] _, acc -> acc
_, acc -> acc end
end, |> :ets.foldl(
[], [],
@table @table
) )

View file

@ -200,7 +200,6 @@ defmodule Microwaveprop.Pskr do
defp grid_center(grid), do: Geo.maidenhead_center(grid) || {0.0, 0.0} defp grid_center(grid), do: Geo.maidenhead_center(grid) || {0.0, 0.0}
defp maybe_mode_list(""), do: [] defp maybe_mode_list(""), do: []
defp maybe_mode_list(nil), do: []
defp maybe_mode_list(mode), do: [mode] defp maybe_mode_list(mode), do: [mode]
defp add_mode(modes, ""), do: modes defp add_mode(modes, ""), do: modes

View file

@ -60,7 +60,7 @@ defmodule Microwaveprop.Pskr.Aggregator do
@impl true @impl true
def init(opts) do def init(opts) do
flush_ms = Keyword.get(opts, :flush_ms, @default_flush_ms) flush_ms = Keyword.get(opts, :flush_ms, @default_flush_ms)
if flush_ms > 0, do: schedule_flush(flush_ms) _ = if flush_ms > 0, do: schedule_flush(flush_ms)
{:ok, %{rows: %{}, parsed: 0, dropped: 0, flush_ms: flush_ms}} {:ok, %{rows: %{}, parsed: 0, dropped: 0, flush_ms: flush_ms}}
end end
@ -95,7 +95,7 @@ defmodule Microwaveprop.Pskr.Aggregator do
Logger.info("Pskr.Aggregator flushed #{written} rows (parsed #{state.parsed}, dropped #{state.dropped})") Logger.info("Pskr.Aggregator flushed #{written} rows (parsed #{state.parsed}, dropped #{state.dropped})")
end end
schedule_flush(state.flush_ms) _ = schedule_flush(state.flush_ms)
{:noreply, %{state | rows: %{}, parsed: 0, dropped: 0}} {:noreply, %{state | rows: %{}, parsed: 0, dropped: 0}}
end end

View file

@ -148,7 +148,7 @@ defmodule Microwaveprop.Pskr.Client do
def handle_info({event, _node}, state) when event in [:nodeup, :nodedown], do: {:noreply, state} def handle_info({event, _node}, state) when event in [:nodeup, :nodedown], do: {:noreply, state}
def handle_info({:tcp, socket, data}, %{socket: socket} = state) do def handle_info({:tcp, socket, data}, %{socket: socket} = state) do
:inet.setopts(socket, active: :once) _ = :inet.setopts(socket, active: :once)
{:noreply, process_buffer(%{state | buffer: state.buffer <> data})} {:noreply, process_buffer(%{state | buffer: state.buffer <> data})}
end end
@ -280,8 +280,8 @@ defmodule Microwaveprop.Pskr.Client do
end end
defp drop_socket(state) do defp drop_socket(state) do
if state.ping_timer, do: Process.cancel_timer(state.ping_timer) _ = if state.ping_timer, do: Process.cancel_timer(state.ping_timer)
if is_port(state.socket), do: :gen_tcp.close(state.socket) _ = if is_port(state.socket), do: :gen_tcp.close(state.socket)
%{state | socket: nil, buffer: <<>>, ping_timer: nil} %{state | socket: nil, buffer: <<>>, ping_timer: nil}
end end

View file

@ -465,7 +465,7 @@ defmodule Microwaveprop.Radio do
defp deg_to_rad(deg), do: deg * :math.pi() / 180 defp deg_to_rad(deg), do: deg * :math.pi() / 180
@spec get_contact!(Ecto.UUID.t()) :: Contact.t() @dialyzer {:no_match, get_contact!: 1}
def get_contact!(id) do def get_contact!(id) do
Contact Contact
|> Repo.get!(id) |> Repo.get!(id)
@ -735,18 +735,16 @@ defmodule Microwaveprop.Radio do
min_ts = DateTime.add(ts, -@dedup_window_seconds, :second) min_ts = DateTime.add(ts, -@dedup_window_seconds, :second)
max_ts = DateTime.add(ts, @dedup_window_seconds, :second) max_ts = DateTime.add(ts, @dedup_window_seconds, :second)
from(c in Contact, Repo.one(
where: from(c in Contact,
c.band == ^band_decimal and where:
c.qso_timestamp >= ^min_ts and c.band == ^band_decimal and c.qso_timestamp >= ^min_ts and c.qso_timestamp <= ^max_ts and
c.qso_timestamp <= ^max_ts and c.flagged_invalid == false and fragment("LEAST(UPPER(station1), UPPER(station2)) = ?", ^min(s1, s2)) and
c.flagged_invalid == false and fragment("GREATEST(UPPER(station1), UPPER(station2)) = ?", ^max(s1, s2)) and
fragment("LEAST(UPPER(station1), UPPER(station2)) = ?", ^min(s1, s2)) and fragment("LEAST(UPPER(grid1), UPPER(grid2)) = ?", ^min(g1, g2)) and
fragment("GREATEST(UPPER(station1), UPPER(station2)) = ?", ^max(s1, s2)) and fragment("GREATEST(UPPER(grid1), UPPER(grid2)) = ?", ^max(g1, g2))
fragment("LEAST(UPPER(grid1), UPPER(grid2)) = ?", ^min(g1, g2)) and )
fragment("GREATEST(UPPER(grid1), UPPER(grid2)) = ?", ^max(g1, g2))
) )
|> Repo.one()
end end
@refinement_fields ~w(grid1 grid2 mode)a @refinement_fields ~w(grid1 grid2 mode)a

View file

@ -10,7 +10,7 @@ defmodule Microwaveprop.Radio.TextSanitizer do
""" """
@spec sanitize(nil) :: nil @spec sanitize(nil) :: nil
@spec sanitize(String.t()) :: String.t() @spec sanitize(String.t()) :: String.t() | {:error, String.t(), String.t()} | {:incomplete, String.t(), String.t()}
def sanitize(nil), do: nil def sanitize(nil), do: nil
def sanitize(""), do: "" def sanitize(""), do: ""

View file

@ -192,7 +192,7 @@ defmodule Microwaveprop.Rover do
end end
defp maybe_enqueue_elevation({:ok, %FixedStation{elevation_m: nil, id: id}} = result) do defp maybe_enqueue_elevation({:ok, %FixedStation{elevation_m: nil, id: id}} = result) do
enqueue_station_elevation(id) _ = enqueue_station_elevation(id)
result result
end end

View file

@ -30,7 +30,7 @@ defmodule Microwaveprop.RoverPlanning do
|> Repo.all() |> Repo.all()
end end
@spec get_mission(Ecto.UUID.t()) :: Mission.t() | nil @spec get_mission(Ecto.UUID.t()) :: Mission.t() | nil | [%{atom() => any()}]
def get_mission(id) when is_binary(id) do def get_mission(id) when is_binary(id) do
case Ecto.UUID.cast(id) do case Ecto.UUID.cast(id) do
{:ok, uuid} -> {:ok, uuid} ->
@ -43,7 +43,7 @@ defmodule Microwaveprop.RoverPlanning do
end end
end end
@spec get_mission_with_paths(Ecto.UUID.t()) :: Mission.t() | nil @spec get_mission_with_paths(Ecto.UUID.t()) :: Mission.t() | nil | [%{atom() => any()}]
def get_mission_with_paths(id) do def get_mission_with_paths(id) do
case get_mission(id) do case get_mission(id) do
nil -> nil nil -> nil
@ -167,9 +167,10 @@ defmodule Microwaveprop.RoverPlanning do
""" """
@spec enqueue_reconcile(Mission.t()) :: :ok @spec enqueue_reconcile(Mission.t()) :: :ok
def enqueue_reconcile(%Mission{id: mission_id}) do def enqueue_reconcile(%Mission{id: mission_id}) do
%{"mission_id" => mission_id} _ =
|> RoverMissionReconcileWorker.new() %{"mission_id" => mission_id}
|> Oban.insert() |> RoverMissionReconcileWorker.new()
|> Oban.insert()
:ok :ok
end end
@ -269,10 +270,11 @@ defmodule Microwaveprop.RoverPlanning do
} }
end) end)
Repo.insert_all(Path, path_rows, _ =
on_conflict: :nothing, Repo.insert_all(Path, path_rows,
conflict_target: [:mission_id, :rover_location_id, :station_id, :band_mhz] on_conflict: :nothing,
) conflict_target: [:mission_id, :rover_location_id, :station_id, :band_mhz]
)
enqueue_jobs(tuples, mission) enqueue_jobs(tuples, mission)
:ok :ok
@ -289,7 +291,7 @@ defmodule Microwaveprop.RoverPlanning do
}) })
end) end)
Oban.insert_all(changesets) _ = Oban.insert_all(changesets)
:ok :ok
end end

View file

@ -457,23 +457,24 @@ defmodule Microwaveprop.Weather do
# Fire-and-forget: run each ANALYZE on the pod's own node so the # Fire-and-forget: run each ANALYZE on the pod's own node so the
# caller returns before any single query finishes. Small tables # caller returns before any single query finishes. Small tables
# first keeps progress visible early. # first keeps progress visible early.
Task.Supervisor.start_child({:via, PartitionSupervisor, {Microwaveprop.TaskSupervisor, self()}}, fn -> _ =
Enum.each(tables, fn t -> Task.Supervisor.start_child({:via, PartitionSupervisor, {Microwaveprop.TaskSupervisor, self()}}, fn ->
Logger.info("Weather.analyze_all: ANALYZE #{t}") Enum.each(tables, fn t ->
started = System.monotonic_time(:millisecond) Logger.info("Weather.analyze_all: ANALYZE #{t}")
started = System.monotonic_time(:millisecond)
try do try do
Repo.query!("ANALYZE #{quote_ident(t)}", [], timeout: :infinity) _ = Repo.query!("ANALYZE #{quote_ident(t)}", [], timeout: :infinity)
elapsed = System.monotonic_time(:millisecond) - started elapsed = System.monotonic_time(:millisecond) - started
Logger.info("Weather.analyze_all: ANALYZE #{t} done in #{elapsed}ms") Logger.info("Weather.analyze_all: ANALYZE #{t} done in #{elapsed}ms")
rescue rescue
e -> Logger.warning("Weather.analyze_all: ANALYZE #{t} failed: #{inspect(e)}") e -> Logger.warning("Weather.analyze_all: ANALYZE #{t} failed: #{inspect(e)}")
end end
end)
Logger.info("Weather.analyze_all: complete (#{length(tables)} tables)")
end) end)
Logger.info("Weather.analyze_all: complete (#{length(tables)} tables)")
end)
%{queued: length(tables), tables: tables} %{queued: length(tables), tables: tables}
end end
@ -539,43 +540,44 @@ defmodule Microwaveprop.Weather do
now = DateTime.truncate(DateTime.utc_now(), :second) now = DateTime.truncate(DateTime.utc_now(), :second)
if updates != [] do _ =
{values_sql, params} = if updates != [] do
updates {values_sql, params} =
|> Enum.with_index() updates
|> Enum.reduce({"", []}, fn u, {sql, params} -> |> Enum.with_index()
base = Enum.count(params) |> Enum.reduce({"", []}, fn u, {sql, params} ->
frag = "($#{base + 1}::uuid, $#{base + 2}, $#{base + 3}, $#{base + 4}, $#{base + 5}, $#{base + 6})" base = Enum.count(params)
sep = if sql == "", do: "", else: ", " frag = "($#{base + 1}::uuid, $#{base + 2}, $#{base + 3}, $#{base + 4}, $#{base + 5}, $#{base + 6})"
sep = if sql == "", do: "", else: ", "
params = params =
params ++ params ++
[ [
Ecto.UUID.dump!(u.id), Ecto.UUID.dump!(u.id),
u.surface_refractivity, u.surface_refractivity,
u.min_refractivity_gradient, u.min_refractivity_gradient,
u.ducting_detected, u.ducting_detected,
u.duct_characteristics || [], u.duct_characteristics || [],
now now
] ]
{sql <> sep <> frag, params} {sql <> sep <> frag, params}
end) end)
Repo.query!( Repo.query!(
"UPDATE hrrr_profiles AS h " <> "UPDATE hrrr_profiles AS h " <>
"SET surface_refractivity = v.surface_refractivity, " <> "SET surface_refractivity = v.surface_refractivity, " <>
"min_refractivity_gradient = v.min_refractivity_gradient, " <> "min_refractivity_gradient = v.min_refractivity_gradient, " <>
"ducting_detected = v.ducting_detected, " <> "ducting_detected = v.ducting_detected, " <>
"duct_characteristics = v.duct_characteristics, " <> "duct_characteristics = v.duct_characteristics, " <>
"updated_at = v.updated_at " <> "updated_at = v.updated_at " <>
"FROM (VALUES #{values_sql}) " <> "FROM (VALUES #{values_sql}) " <>
"AS v(id, surface_refractivity, min_refractivity_gradient, " <> "AS v(id, surface_refractivity, min_refractivity_gradient, " <>
"ducting_detected, duct_characteristics, updated_at) " <> "ducting_detected, duct_characteristics, updated_at) " <>
"WHERE h.id = v.id", "WHERE h.id = v.id",
params params
) )
end end
length(updates) length(updates)
end end

View file

@ -657,7 +657,7 @@ defmodule Microwaveprop.Weather.Grib2.Wgrib2 do
# Parse "lon=242.958,lat=32.938,val=306.5". Exposed (with @doc false) # Parse "lon=242.958,lat=32.938,val=306.5". Exposed (with @doc false)
# so the integer-lon edge case (wgrib2 occasionally drops the trailing # so the integer-lon edge case (wgrib2 occasionally drops the trailing
# `.0`) can be asserted directly. # `.0`) can be asserted directly.
@spec parse_lon_val_segment(String.t()) :: %{lon: float(), lat: float(), d: float(), elev_m: float()} | nil @spec parse_lon_val_segment(String.t()) :: {float(), float(), float()} | nil
def parse_lon_val_segment(segment) do def parse_lon_val_segment(segment) do
case Regex.run(~r/lon=([\d.]+),lat=([\d.]+),val=([\d.eE+-]+)/, segment) do case Regex.run(~r/lon=([\d.]+),lat=([\d.]+),val=([\d.eE+-]+)/, segment) do
[_, lon_str, lat_str, val_str] -> [_, lon_str, lat_str, val_str] ->

View file

@ -184,7 +184,7 @@ defmodule Microwaveprop.Weather.GridCache do
def handle_call(:sync, _from, state), do: {:reply, :ok, state} def handle_call(:sync, _from, state), do: {:reply, :ok, state}
def handle_call(:clear, _from, state) do def handle_call(:clear, _from, state) do
if valkey_backend?(), do: valkey_clear(), else: :ets.delete_all_objects(@table) _ = if valkey_backend?(), do: valkey_clear(), else: :ets.delete_all_objects(@table)
for {ref, _vt} <- state.monitors, do: Process.demonitor(ref, [:flush]) for {ref, _vt} <- state.monitors, do: Process.demonitor(ref, [:flush])
:ets.delete_all_objects(@lock_table) :ets.delete_all_objects(@lock_table)
@ -291,10 +291,14 @@ defmodule Microwaveprop.Weather.GridCache do
end end
defp ets_latest_valid_time do defp ets_latest_valid_time do
:ets.foldl(fn :ets.foldl(
{vt, _}, nil -> vt fn
{vt, _}, latest -> if DateTime.compare(vt, latest) == :gt, do: vt, else: latest {vt, _}, nil -> vt
end, nil, @table) {vt, _}, latest -> if DateTime.after?(vt, latest), do: vt, else: latest
end,
nil,
@table
)
end end
defp ets_prune_keep_latest(keep) do defp ets_prune_keep_latest(keep) do
@ -475,12 +479,13 @@ defmodule Microwaveprop.Weather.GridCache do
end end
defp valkey_clear do defp valkey_clear do
case Valkey.scan_match("#{@valkey_chunk_prefix}*") do _ =
{:ok, keys} -> Valkey.del(keys) case Valkey.scan_match("#{@valkey_chunk_prefix}*") do
_ -> :ok {:ok, keys} -> _ = Valkey.del(keys)
end _ -> :ok
end
Valkey.del([@valkey_index_key]) _ = Valkey.del([@valkey_index_key])
end end
defp chunk_key_pattern(valid_time) do defp chunk_key_pattern(valid_time) do

View file

@ -57,10 +57,14 @@ defmodule Microwaveprop.Weather.NexradCache do
# Iterate ETS with foldl so frame payloads (~66 MB each) never land on the # Iterate ETS with foldl so frame payloads (~66 MB each) never land on the
# calling process's heap — :ets.tab2list() at capacity would allocate ~1.3 GB. # calling process's heap — :ets.tab2list() at capacity would allocate ~1.3 GB.
defp oldest_key do defp oldest_key do
:ets.foldl(fn :ets.foldl(
{ts, _, _}, nil -> ts fn
{ts, _, _}, oldest -> if DateTime.before?(ts, oldest), do: ts, else: oldest {ts, _, _}, nil -> ts
end, nil, @table) {ts, _, _}, oldest -> if DateTime.before?(ts, oldest), do: ts, else: oldest
end,
nil,
@table
)
end end
@spec prune_older_than(DateTime.t()) :: non_neg_integer() @spec prune_older_than(DateTime.t()) :: non_neg_integer()

View file

@ -134,9 +134,7 @@ defmodule Microwaveprop.Weather.ScalarFile do
# leave stale chunks behind. # leave stale chunks behind.
case File.ls(dir) do case File.ls(dir) do
{:ok, entries} -> {:ok, entries} ->
for entry <- entries do Enum.each(entries, &File.rm!(Path.join(dir, &1)))
_ = File.rm(Path.join(dir, entry))
end
_ -> _ ->
:ok :ok

View file

@ -109,7 +109,7 @@ defmodule Microwaveprop.Workers.RoverPathProfileWorker do
rescue rescue
e -> e ->
Logger.error("RoverPathProfileWorker crashed for path=#{path.id}: #{Exception.message(e)}") Logger.error("RoverPathProfileWorker crashed for path=#{path.id}: #{Exception.message(e)}")
store_failed(path, "exception: #{Exception.message(e)}") _ = store_failed(path, "exception: #{Exception.message(e)}")
reraise e, __STACKTRACE__ reraise e, __STACKTRACE__
end end
@ -128,7 +128,7 @@ defmodule Microwaveprop.Workers.RoverPathProfileWorker do
|> Repo.update() |> Repo.update()
|> case do |> case do
{:ok, updated} -> {:ok, updated} ->
broadcast(updated) _ = broadcast(updated)
:ok :ok
{:error, cs} -> {:error, cs} ->
@ -216,7 +216,7 @@ defmodule Microwaveprop.Workers.RoverPathProfileWorker do
|> Repo.update() |> Repo.update()
|> case do |> case do
{:ok, updated} -> {:ok, updated} ->
broadcast(updated) _ = broadcast(updated)
{:error, message} {:error, message}
{:error, cs} -> {:error, cs} ->

View file

@ -50,13 +50,14 @@ defmodule MicrowavepropWeb.Api.RateLimiter do
""" """
@spec init_table() :: :ok @spec init_table() :: :ok
def init_table do def init_table do
if :ets.whereis(@table) == :undefined do _ =
try do if :ets.whereis(@table) == :undefined do
:ets.new(@table, [:set, :public, :named_table, write_concurrency: true]) try do
rescue :ets.new(@table, [:set, :public, :named_table, write_concurrency: true])
ArgumentError -> :ok rescue
ArgumentError -> :ok
end
end end
end
:ok :ok
end end

View file

@ -105,13 +105,12 @@ defmodule MicrowavepropWeb.ScoresController do
{lats, lons, score_bytes} = {lats, lons, score_bytes} =
Enum.reduce(scores, {[], [], []}, fn s, {lats_acc, lons_acc, scs_acc} -> Enum.reduce(scores, {[], [], []}, fn s, {lats_acc, lons_acc, scs_acc} ->
{[<<s.lat * 1.0::little-float-32>> | lats_acc], {[<<s.lat * 1.0::little-float-32>> | lats_acc], [<<s.lon * 1.0::little-float-32>> | lons_acc],
[<<s.lon * 1.0::little-float-32>> | lons_acc],
[<<clamp_score(s.score)::8>> | scs_acc]} [<<clamp_score(s.score)::8>> | scs_acc]}
end) end)
<<header::binary, IO.iodata_to_binary(lats)::binary, <<header::binary, IO.iodata_to_binary(lats)::binary, IO.iodata_to_binary(lons)::binary,
IO.iodata_to_binary(lons)::binary, IO.iodata_to_binary(score_bytes)::binary>> IO.iodata_to_binary(score_bytes)::binary>>
end end
defp clamp_score(s) when is_integer(s) and s < 0, do: 0 defp clamp_score(s) when is_integer(s) and s < 0, do: 0

View file

@ -95,22 +95,22 @@ defmodule MicrowavepropWeb.ContactLive.Index do
|> Repo.all() |> Repo.all()
|> Map.new() |> Map.new()
counts = Enum.map(1..12, fn m -> {m, Map.get(by_month, m, 0)} end) counts = Enum.map(1..12, fn m -> {m, Map.get(by_month, m, 0)} end)
max_count = counts |> Enum.map(&elem(&1, 1)) |> Enum.max() max_count = counts |> Enum.map(&elem(&1, 1)) |> Enum.max()
Enum.map(counts, fn {month, count} -> Enum.map(counts, fn {month, count} ->
x = (month - 1) * @chart_bar_pitch + @chart_bar_offset x = (month - 1) * @chart_bar_pitch + @chart_bar_offset
height = bar_height(count, max_count) height = bar_height(count, max_count)
%{ %{
month: month, month: month,
label: Enum.at(@month_labels, month - 1), label: Enum.at(@month_labels, month - 1),
count: count, count: count,
x: x, x: x,
y: @chart_baseline - height, y: @chart_baseline - height,
height: height height: height
} }
end) end)
end) end)
end end

View file

@ -225,7 +225,7 @@ defmodule MicrowavepropWeb.ContactLive.Show do
def handle_event("delete_contact", _params, socket) do def handle_event("delete_contact", _params, socket) do
if admin?(socket.assigns) do if admin?(socket.assigns) do
Radio.delete_contact!(socket.assigns.contact) _ = Radio.delete_contact!(socket.assigns.contact)
{:noreply, {:noreply,
socket socket

View file

@ -24,9 +24,10 @@ defmodule MicrowavepropWeb.RoverPlanningLive.Show do
|> push_navigate(to: ~p"/rover-planning")} |> push_navigate(to: ~p"/rover-planning")}
%Mission{} = mission -> %Mission{} = mission ->
if connected?(socket) do _ =
Phoenix.PubSub.subscribe(Microwaveprop.PubSub, "rover_planning:#{mission.id}") if connected?(socket) do
end Phoenix.PubSub.subscribe(Microwaveprop.PubSub, "rover_planning:#{mission.id}")
end
{:ok, {:ok,
assign(socket, assign(socket,

View file

@ -70,9 +70,6 @@ defmodule MicrowavepropWeb.SkewtLocationResolver do
{:error, reason} when is_binary(reason) -> {:error, reason} when is_binary(reason) ->
{:error, "callsign lookup failed: #{reason}"} {:error, "callsign lookup failed: #{reason}"}
{:error, _} ->
{:error, "callsign lookup failed"}
end end
end end
@ -83,9 +80,6 @@ defmodule MicrowavepropWeb.SkewtLocationResolver do
{:error, reason} when is_binary(reason) -> {:error, reason} when is_binary(reason) ->
{:error, "geocode failed: #{reason}"} {:error, "geocode failed: #{reason}"}
{:error, _} ->
{:error, "geocode failed"}
end end
end end
end end

View file

@ -47,7 +47,7 @@ defmodule MicrowavepropWeb.SkewtSvg do
ascent line is overlaid on the temperature trace, matching the ascent line is overlaid on the temperature trace, matching the
surface-parcel rendition the SPC sounding viewer publishes. surface-parcel rendition the SPC sounding viewer publishes.
""" """
@spec render([map()], keyword()) :: iodata() @spec render([map()], keyword()) :: list()
def render(profile, opts \\ []) when is_list(profile) do def render(profile, opts \\ []) when is_list(profile) do
cleaned = clean_profile(profile) cleaned = clean_profile(profile)
parcel = Keyword.get(opts, :parcel_trace, []) parcel = Keyword.get(opts, :parcel_trace, [])

View file

@ -220,5 +220,4 @@ defmodule Mix.Tasks.Calibrate.Aprs144 do
end end
defp format_float(f) when is_float(f), do: :erlang.float_to_binary(f, decimals: 4) defp format_float(f) when is_float(f), do: :erlang.float_to_binary(f, decimals: 4)
defp format_float(f), do: to_string(f)
end end

View file

@ -40,6 +40,8 @@ defmodule Mix.Tasks.Unused do
use Mix.Task use Mix.Task
@dialyzer {:no_opaque, run: 1}
# Callbacks that are invoked by the runtime, not by direct function calls. # Callbacks that are invoked by the runtime, not by direct function calls.
# They look unused to static analysis but are actually called by OTP / # They look unused to static analysis but are actually called by OTP /
# Phoenix / LiveView machinery. # Phoenix / LiveView machinery.
@ -376,6 +378,7 @@ defmodule Mix.Tasks.Unused do
end end
end end
@spec analyze_beam(String.t()) :: {MapSet.t(), MapSet.t()}
defp analyze_beam(beam_file) do defp analyze_beam(beam_file) do
module = beam_module(beam_file) module = beam_module(beam_file)
@ -388,7 +391,7 @@ defmodule Mix.Tasks.Unused do
{defs, calls} {defs, calls}
{:error, _reason} -> _ ->
# Binary module or unreadable — skip # Binary module or unreadable — skip
{MapSet.new(), MapSet.new()} {MapSet.new(), MapSet.new()}
end end

View file

@ -82,7 +82,6 @@
"text_diff": {:hex, :text_diff, "0.1.0", "1caf3175e11a53a9a139bc9339bd607c47b9e376b073d4571c031913317fecaa", [:mix], [], "hexpm", "d1ffaaecab338e49357b6daa82e435f877e0649041ace7755583a0ea3362dbd7"}, "text_diff": {:hex, :text_diff, "0.1.0", "1caf3175e11a53a9a139bc9339bd607c47b9e376b073d4571c031913317fecaa", [:mix], [], "hexpm", "d1ffaaecab338e49357b6daa82e435f877e0649041ace7755583a0ea3362dbd7"},
"thousand_island": {:hex, :thousand_island, "1.5.0", "f50a213cac97262b6d5ebb85745aa2c00fec1413191e6e66834788d45425cecb", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "708923d40523e43cf99041ab37a0d4b0ec426ac6438fa3716ab23d919eaeb412"}, "thousand_island": {:hex, :thousand_island, "1.5.0", "f50a213cac97262b6d5ebb85745aa2c00fec1413191e6e66834788d45425cecb", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "708923d40523e43cf99041ab37a0d4b0ec426ac6438fa3716ab23d919eaeb412"},
"tidewave": {:hex, :tidewave, "0.5.6", "91f35540b5599640443f1d3a1c6166bf506e202840261a6344e384e8813c1f64", [:mix], [{:circular_buffer, "~> 0.4 or ~> 1.0", [hex: :circular_buffer, repo: "hexpm", optional: false]}, {:igniter, "~> 0.6", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:phoenix_live_reload, ">= 1.6.1", [hex: :phoenix_live_reload, repo: "hexpm", optional: true]}, {:plug, "~> 1.17", [hex: :plug, repo: "hexpm", optional: false]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}], "hexpm", "dc82d52b8b6ffc04680544b17cd340c7d4166bb0d63999eb960850526866b533"}, "tidewave": {:hex, :tidewave, "0.5.6", "91f35540b5599640443f1d3a1c6166bf506e202840261a6344e384e8813c1f64", [:mix], [{:circular_buffer, "~> 0.4 or ~> 1.0", [hex: :circular_buffer, repo: "hexpm", optional: false]}, {:igniter, "~> 0.6", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:phoenix_live_reload, ">= 1.6.1", [hex: :phoenix_live_reload, repo: "hexpm", optional: true]}, {:plug, "~> 1.17", [hex: :plug, repo: "hexpm", optional: false]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}], "hexpm", "dc82d52b8b6ffc04680544b17cd340c7d4166bb0d63999eb960850526866b533"},
"unicode_util_compat": {:hex, :unicode_util_compat, "0.7.1", "a48703a25c170eedadca83b11e88985af08d35f37c6f664d6dcfb106a97782fc", [:rebar3], [], "hexpm", "b3a917854ce3ae233619744ad1e0102e05673136776fb2fa76234f3e03b23642"},
"uniq": {:hex, :uniq, "0.6.3", "68acff834cce1817b52928ef346662735c5413a4fec9c3b0d4a9126de5b2b489", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm", "2b2a900d0a20f3a55d3de0bc8150495e4a71255734dfb23889991bda5aca6c7d"}, "uniq": {:hex, :uniq, "0.6.3", "68acff834cce1817b52928ef346662735c5413a4fec9c3b0d4a9126de5b2b489", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm", "2b2a900d0a20f3a55d3de0bc8150495e4a71255734dfb23889991bda5aca6c7d"},
"websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"}, "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"},
"websock_adapter": {:hex, :websock_adapter, "0.5.9", "43dc3ba6d89ef5dec5b1d0a39698436a1e856d000d84bf31a3149862b01a287f", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "5534d5c9adad3c18a0f58a9371220d75a803bf0b9a3d87e6fe072faaeed76a08"}, "websock_adapter": {:hex, :websock_adapter, "0.5.9", "43dc3ba6d89ef5dec5b1d0a39698436a1e856d000d84bf31a3149862b01a287f", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "5534d5c9adad3c18a0f58a9371220d75a803bf0b9a3d87e6fe072faaeed76a08"},

View file

@ -13,26 +13,26 @@ defmodule Microwaveprop.Repo.Migrations.AddPerformanceIndexes do
# hrrr_profiles.surface_refractivity IS NULL — backfill query scans for nulls # hrrr_profiles.surface_refractivity IS NULL — backfill query scans for nulls
create index(:hrrr_profiles, [:surface_refractivity], create index(:hrrr_profiles, [:surface_refractivity],
where: "surface_refractivity IS NULL", where: "surface_refractivity IS NULL",
name: "hrrr_profiles_surface_refractivity_null_idx" name: "hrrr_profiles_surface_refractivity_null_idx"
) )
# hrrr_native_profiles.bulk_richardson IS NULL — derive task scans for nulls # hrrr_native_profiles.bulk_richardson IS NULL — derive task scans for nulls
create index(:hrrr_native_profiles, [:bulk_richardson], create index(:hrrr_native_profiles, [:bulk_richardson],
where: "bulk_richardson IS NULL AND level_count > 2", where: "bulk_richardson IS NULL AND level_count > 2",
name: "hrrr_native_profiles_bulk_richardson_null_idx" name: "hrrr_native_profiles_bulk_richardson_null_idx"
) )
# grid_tasks.claimed_at for status='running' — reclaim-stale query # grid_tasks.claimed_at for status='running' — reclaim-stale query
create index(:grid_tasks, [:claimed_at], create index(:grid_tasks, [:claimed_at],
where: "status = 'running'", where: "status = 'running'",
name: "grid_tasks_running_claimed_at_idx" name: "grid_tasks_running_claimed_at_idx"
) )
# rover_mission_paths composite — worker lookup on all 4 columns # rover_mission_paths composite — worker lookup on all 4 columns
create index(:rover_mission_paths, [:mission_id, :rover_location_id, :station_id, :band_mhz], create index(:rover_mission_paths, [:mission_id, :rover_location_id, :station_id, :band_mhz],
name: "rover_mission_paths_lookup_idx" name: "rover_mission_paths_lookup_idx"
) )
# fixed_stations(user_id, position, inserted_at) — WHERE + ORDER BY # fixed_stations(user_id, position, inserted_at) — WHERE + ORDER BY
create index(:fixed_stations, [:user_id, :position, :inserted_at]) create index(:fixed_stations, [:user_id, :position, :inserted_at])