diff --git a/lib/microwaveprop/accounts.ex b/lib/microwaveprop/accounts.ex index 01d6d9b5..54b8a211 100644 --- a/lib/microwaveprop/accounts.ex +++ b/lib/microwaveprop/accounts.ex @@ -137,13 +137,14 @@ defmodule Microwaveprop.Accounts do end 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 - _ = %{user_id: id} |> worker.new() |> Oban.insert() + if Code.ensure_loaded?(worker) and function_exported?(worker, :new, 1) do + _ = %{user_id: id} |> worker.new() |> Oban.insert() + end end - end {:ok, user} end @@ -163,14 +164,15 @@ defmodule Microwaveprop.Accounts do def backfill_missing_home_qth do worker = UserHomeQthLookupWorker - if home_qth_lookup_enabled?() and Code.ensure_loaded?(worker) and function_exported?(worker, :new, 1) do - User - |> where([u], is_nil(u.home_grid)) - |> select([u], u.id) - |> Repo.all() - |> Enum.map(fn id -> %{user_id: id} |> worker.new() end) - |> then(&if &1 != [], do: Oban.insert_all(&1)) - end + _ = + if home_qth_lookup_enabled?() and Code.ensure_loaded?(worker) and function_exported?(worker, :new, 1) do + User + |> where([u], is_nil(u.home_grid)) + |> select([u], u.id) + |> Repo.all() + |> Enum.map(fn id -> worker.new(%{user_id: id}) end) + |> then(&if &1 != [], do: Oban.insert_all(&1)) + end :ok end diff --git a/lib/microwaveprop/accounts/user_token.ex b/lib/microwaveprop/accounts/user_token.ex index cd4af0e5..8d40b79e 100644 --- a/lib/microwaveprop/accounts/user_token.ex +++ b/lib/microwaveprop/accounts/user_token.ex @@ -75,7 +75,7 @@ defmodule Microwaveprop.Accounts.UserToken do The token is valid if it matches the value in the database and it has 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 query = from token in by_token_and_context_query(token, "session"), diff --git a/lib/microwaveprop/application.ex b/lib/microwaveprop/application.ex index 1696da37..ec204aa3 100644 --- a/lib/microwaveprop/application.ex +++ b/lib/microwaveprop/application.ex @@ -79,15 +79,16 @@ defmodule Microwaveprop.Application do # Task so the application supervisor doesn't block on long-running # migrations on pod boot. Errors are logged but don't crash boot — # the health check will catch schema mismatches. - Task.start(fn -> - try do - results = Microwaveprop.Release.migrate() - Logger.info("Database migrations completed (#{length(results)} repos)") - rescue - e -> - Logger.error("Database migrations failed: #{Exception.format(:error, e, __STACKTRACE__)}") - end - end) + _ = + Task.start(fn -> + try do + results = Microwaveprop.Release.migrate() + Logger.info("Database migrations completed (#{length(results)} repos)") + rescue + e -> + Logger.error("Database migrations failed: #{Exception.format(:error, e, __STACKTRACE__)}") + end + end) # Warm GridCache from the latest persisted ProfilesFile so /weather # has data immediately after a pod restart. Runs after the GridCache @@ -123,17 +124,18 @@ defmodule Microwaveprop.Application do end) # 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} = - Task.start(fn -> - try do - Microwaveprop.Propagation.load_ml_model() - rescue - e -> - Logger.error("Application: ML model load failed: #{Exception.format(:error, e, __STACKTRACE__)}") - end - end) - end + _ = + if Application.get_env(:microwaveprop, :load_ml_model, false) do + {:ok, _pid} = + Task.start(fn -> + try do + Microwaveprop.Propagation.load_ml_model() + rescue + e -> + Logger.error("Application: ML model load failed: #{Exception.format(:error, e, __STACKTRACE__)}") + end + end) + end result end diff --git a/lib/microwaveprop/backtest/features.ex b/lib/microwaveprop/backtest/features.ex index d492b1c6..d433d199 100644 --- a/lib/microwaveprop/backtest/features.ex +++ b/lib/microwaveprop/backtest/features.ex @@ -283,15 +283,15 @@ defmodule Microwaveprop.Backtest.Features do def parallel_to_front(_lat, _lon, _valid_time), do: nil @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) @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) @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) @doc """ diff --git a/lib/microwaveprop/buildings/index.ex b/lib/microwaveprop/buildings/index.ex index 6b307230..b1491b53 100644 --- a/lib/microwaveprop/buildings/index.ex +++ b/lib/microwaveprop/buildings/index.ex @@ -36,7 +36,7 @@ defmodule Microwaveprop.Buildings.Index do Same scan path as `max_height_near/3` — useful for surfacing the 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 ensure_tables() 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 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 ensure_tables() @@ -145,13 +145,17 @@ defmodule Microwaveprop.Buildings.Index do end defp ensure_tables do - if :ets.whereis(@table) == :undefined do - :ets.new(@table, [:set, :named_table, :public, read_concurrency: true]) - end + _ = + if :ets.whereis(@table) == :undefined do + :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]) - end + _ = + if :ets.whereis(@loaded_tiles_table) == :undefined do + :ets.new(@loaded_tiles_table, [:set, :named_table, :public, read_concurrency: true]) + end + + :ok end defp bucket_range(min, max) do diff --git a/lib/microwaveprop/buildings/parser.ex b/lib/microwaveprop/buildings/parser.ex index edc58336..fdead40e 100644 --- a/lib/microwaveprop/buildings/parser.ex +++ b/lib/microwaveprop/buildings/parser.ex @@ -23,7 +23,7 @@ defmodule Microwaveprop.Buildings.Parser do 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. """ - @spec parse_tile(String.t()) :: Enumerable.t() + @dialyzer {:no_return, parse_tile: 1} def parse_tile(path) do path |> File.stream!([:read, :compressed]) diff --git a/lib/microwaveprop/cache.ex b/lib/microwaveprop/cache.ex index c10b0386..01c3bebc 100644 --- a/lib/microwaveprop/cache.ex +++ b/lib/microwaveprop/cache.ex @@ -58,7 +58,7 @@ defmodule Microwaveprop.Cache do Prefer `invalidate/1` for single-key deletions; use this when you 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 :ets.match_delete(@table, pattern) :ok diff --git a/lib/microwaveprop/markdown.ex b/lib/microwaveprop/markdown.ex index b5e11ba3..0b88f90d 100644 --- a/lib/microwaveprop/markdown.ex +++ b/lib/microwaveprop/markdown.ex @@ -93,7 +93,8 @@ defmodule Microwaveprop.Markdown do item = binary_part(line, 2, byte_size(line) - 2) 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 take_list(rest, [prev <> " " <> String.trim(line) | items]) diff --git a/lib/microwaveprop/oban_error_reporter.ex b/lib/microwaveprop/oban_error_reporter.ex index 54f1e8b7..bd173dfb 100644 --- a/lib/microwaveprop/oban_error_reporter.ex +++ b/lib/microwaveprop/oban_error_reporter.ex @@ -55,8 +55,11 @@ defmodule Microwaveprop.ObanErrorReporter do :ok end + @typedoc false + @type telemetry_event :: [atom(), ...] + @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 do_handle(measurements, metadata) rescue diff --git a/lib/microwaveprop/partition_manager.ex b/lib/microwaveprop/partition_manager.ex index 57905d5a..3a4e6cfe 100644 --- a/lib/microwaveprop/partition_manager.ex +++ b/lib/microwaveprop/partition_manager.ex @@ -83,12 +83,13 @@ defmodule Microwaveprop.PartitionManager do if covered_in_memory?(existing, q_start_dt, q_end_dt) do {parent, name, :exists} else - Repo.query!(""" - CREATE TABLE public."#{name}" - PARTITION OF public."#{parent}" - FOR VALUES FROM ('#{Date.to_iso8601(q_start)} 00:00:00') - TO ('#{Date.to_iso8601(q_end)} 00:00:00') - """) + _ = + Repo.query!(""" + CREATE TABLE public."#{name}" + PARTITION OF public."#{parent}" + FOR VALUES FROM ('#{Date.to_iso8601(q_start)} 00:00:00') + TO ('#{Date.to_iso8601(q_end)} 00:00:00') + """) {parent, name, :created} end diff --git a/lib/microwaveprop/propagation/mode_thresholds.ex b/lib/microwaveprop/propagation/mode_thresholds.ex index bfb1282d..ecdaa6e2 100644 --- a/lib/microwaveprop/propagation/mode_thresholds.ex +++ b/lib/microwaveprop/propagation/mode_thresholds.ex @@ -30,6 +30,6 @@ defmodule Microwaveprop.Propagation.ModeThresholds do @spec modes() :: [{mode(), String.t()}] def modes, do: @modes - @spec default_mode() :: mode() + @spec default_mode() :: :ssb def default_mode, do: :ssb end diff --git a/lib/microwaveprop/propagation/notify_listener.ex b/lib/microwaveprop/propagation/notify_listener.ex index e4433eb9..a28489f3 100644 --- a/lib/microwaveprop/propagation/notify_listener.ex +++ b/lib/microwaveprop/propagation/notify_listener.ex @@ -131,14 +131,15 @@ defmodule Microwaveprop.Propagation.NotifyListener do # Spawn in a try/rescue so a crash in the materializer does not # propagate through the linked Task and kill the NotifyListener # GenServer. - Task.start(fn -> - try do - Weather.materialize_scalar_file(valid_time) - rescue - e -> - Logger.error("NotifyListener: scalar materialization crashed: #{Exception.format(:error, e, __STACKTRACE__)}") - end - end) + _ = + Task.start(fn -> + try do + Weather.materialize_scalar_file(valid_time) + rescue + e -> + Logger.error("NotifyListener: scalar materialization crashed: #{Exception.format(:error, e, __STACKTRACE__)}") + end + end) :ok end diff --git a/lib/microwaveprop/propagation/path_compute.ex b/lib/microwaveprop/propagation/path_compute.ex index 5b2c1b16..8118f447 100644 --- a/lib/microwaveprop/propagation/path_compute.ex +++ b/lib/microwaveprop/propagation/path_compute.ex @@ -267,13 +267,6 @@ defmodule Microwaveprop.Propagation.PathCompute do "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 end end diff --git a/lib/microwaveprop/propagation/score_cache.ex b/lib/microwaveprop/propagation/score_cache.ex index 553e8c85..26b7680f 100644 --- a/lib/microwaveprop/propagation/score_cache.ex +++ b/lib/microwaveprop/propagation/score_cache.ex @@ -149,11 +149,11 @@ defmodule Microwaveprop.Propagation.ScoreCache do @doc "Returns the sorted list of valid_times cached for `band_mhz`." @spec valid_times(non_neg_integer()) :: [DateTime.t()] def valid_times(band_mhz) do - :ets.foldl( - fn - {{^band_mhz, vt}, _}, acc -> [vt | acc] - _, acc -> acc - end, + fn + {{^band_mhz, vt}, _}, acc -> [vt | acc] + _, acc -> acc + end + |> :ets.foldl( [], @table ) diff --git a/lib/microwaveprop/pskr.ex b/lib/microwaveprop/pskr.ex index 7980ff55..0357fae8 100644 --- a/lib/microwaveprop/pskr.ex +++ b/lib/microwaveprop/pskr.ex @@ -200,7 +200,6 @@ defmodule Microwaveprop.Pskr do defp grid_center(grid), do: Geo.maidenhead_center(grid) || {0.0, 0.0} defp maybe_mode_list(""), do: [] - defp maybe_mode_list(nil), do: [] defp maybe_mode_list(mode), do: [mode] defp add_mode(modes, ""), do: modes diff --git a/lib/microwaveprop/pskr/aggregator.ex b/lib/microwaveprop/pskr/aggregator.ex index 883bfe2e..c154ad43 100644 --- a/lib/microwaveprop/pskr/aggregator.ex +++ b/lib/microwaveprop/pskr/aggregator.ex @@ -60,7 +60,7 @@ defmodule Microwaveprop.Pskr.Aggregator do @impl true def init(opts) do 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}} end @@ -95,7 +95,7 @@ defmodule Microwaveprop.Pskr.Aggregator do Logger.info("Pskr.Aggregator flushed #{written} rows (parsed #{state.parsed}, dropped #{state.dropped})") end - schedule_flush(state.flush_ms) + _ = schedule_flush(state.flush_ms) {:noreply, %{state | rows: %{}, parsed: 0, dropped: 0}} end diff --git a/lib/microwaveprop/pskr/client.ex b/lib/microwaveprop/pskr/client.ex index 50bdd053..27070f40 100644 --- a/lib/microwaveprop/pskr/client.ex +++ b/lib/microwaveprop/pskr/client.ex @@ -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({: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})} end @@ -280,8 +280,8 @@ defmodule Microwaveprop.Pskr.Client do end defp drop_socket(state) do - if state.ping_timer, do: Process.cancel_timer(state.ping_timer) - if is_port(state.socket), do: :gen_tcp.close(state.socket) + _ = if state.ping_timer, do: Process.cancel_timer(state.ping_timer) + _ = if is_port(state.socket), do: :gen_tcp.close(state.socket) %{state | socket: nil, buffer: <<>>, ping_timer: nil} end diff --git a/lib/microwaveprop/radio.ex b/lib/microwaveprop/radio.ex index a382cec9..da17edec 100644 --- a/lib/microwaveprop/radio.ex +++ b/lib/microwaveprop/radio.ex @@ -465,7 +465,7 @@ defmodule Microwaveprop.Radio do 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 Contact |> Repo.get!(id) @@ -735,18 +735,16 @@ defmodule Microwaveprop.Radio do min_ts = DateTime.add(ts, -@dedup_window_seconds, :second) max_ts = DateTime.add(ts, @dedup_window_seconds, :second) - from(c in Contact, - where: - c.band == ^band_decimal and - c.qso_timestamp >= ^min_ts and - c.qso_timestamp <= ^max_ts and - c.flagged_invalid == false and - fragment("LEAST(UPPER(station1), UPPER(station2)) = ?", ^min(s1, s2)) and - fragment("GREATEST(UPPER(station1), UPPER(station2)) = ?", ^max(s1, s2)) and - fragment("LEAST(UPPER(grid1), UPPER(grid2)) = ?", ^min(g1, g2)) and - fragment("GREATEST(UPPER(grid1), UPPER(grid2)) = ?", ^max(g1, g2)) + Repo.one( + from(c in Contact, + where: + c.band == ^band_decimal and c.qso_timestamp >= ^min_ts and c.qso_timestamp <= ^max_ts and + c.flagged_invalid == false and fragment("LEAST(UPPER(station1), UPPER(station2)) = ?", ^min(s1, s2)) and + fragment("GREATEST(UPPER(station1), UPPER(station2)) = ?", ^max(s1, s2)) and + fragment("LEAST(UPPER(grid1), UPPER(grid2)) = ?", ^min(g1, g2)) and + fragment("GREATEST(UPPER(grid1), UPPER(grid2)) = ?", ^max(g1, g2)) + ) ) - |> Repo.one() end @refinement_fields ~w(grid1 grid2 mode)a diff --git a/lib/microwaveprop/radio/text_sanitizer.ex b/lib/microwaveprop/radio/text_sanitizer.ex index 2cb4b1a9..5f50cc07 100644 --- a/lib/microwaveprop/radio/text_sanitizer.ex +++ b/lib/microwaveprop/radio/text_sanitizer.ex @@ -10,7 +10,7 @@ defmodule Microwaveprop.Radio.TextSanitizer do """ @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(""), do: "" diff --git a/lib/microwaveprop/rover.ex b/lib/microwaveprop/rover.ex index 0c59396d..7762bd23 100644 --- a/lib/microwaveprop/rover.ex +++ b/lib/microwaveprop/rover.ex @@ -192,7 +192,7 @@ defmodule Microwaveprop.Rover do end defp maybe_enqueue_elevation({:ok, %FixedStation{elevation_m: nil, id: id}} = result) do - enqueue_station_elevation(id) + _ = enqueue_station_elevation(id) result end diff --git a/lib/microwaveprop/rover_planning.ex b/lib/microwaveprop/rover_planning.ex index a9ef5231..54c741bb 100644 --- a/lib/microwaveprop/rover_planning.ex +++ b/lib/microwaveprop/rover_planning.ex @@ -30,7 +30,7 @@ defmodule Microwaveprop.RoverPlanning do |> Repo.all() 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 case Ecto.UUID.cast(id) do {:ok, uuid} -> @@ -43,7 +43,7 @@ defmodule Microwaveprop.RoverPlanning do 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 case get_mission(id) do nil -> nil @@ -167,9 +167,10 @@ defmodule Microwaveprop.RoverPlanning do """ @spec enqueue_reconcile(Mission.t()) :: :ok def enqueue_reconcile(%Mission{id: mission_id}) do - %{"mission_id" => mission_id} - |> RoverMissionReconcileWorker.new() - |> Oban.insert() + _ = + %{"mission_id" => mission_id} + |> RoverMissionReconcileWorker.new() + |> Oban.insert() :ok end @@ -269,10 +270,11 @@ defmodule Microwaveprop.RoverPlanning do } end) - Repo.insert_all(Path, path_rows, - on_conflict: :nothing, - conflict_target: [:mission_id, :rover_location_id, :station_id, :band_mhz] - ) + _ = + Repo.insert_all(Path, path_rows, + on_conflict: :nothing, + conflict_target: [:mission_id, :rover_location_id, :station_id, :band_mhz] + ) enqueue_jobs(tuples, mission) :ok @@ -289,7 +291,7 @@ defmodule Microwaveprop.RoverPlanning do }) end) - Oban.insert_all(changesets) + _ = Oban.insert_all(changesets) :ok end diff --git a/lib/microwaveprop/weather.ex b/lib/microwaveprop/weather.ex index 9371419e..a4d94842 100644 --- a/lib/microwaveprop/weather.ex +++ b/lib/microwaveprop/weather.ex @@ -457,23 +457,24 @@ defmodule Microwaveprop.Weather do # Fire-and-forget: run each ANALYZE on the pod's own node so the # caller returns before any single query finishes. Small tables # first keeps progress visible early. - Task.Supervisor.start_child({:via, PartitionSupervisor, {Microwaveprop.TaskSupervisor, self()}}, fn -> - Enum.each(tables, fn t -> - Logger.info("Weather.analyze_all: ANALYZE #{t}") - started = System.monotonic_time(:millisecond) + _ = + Task.Supervisor.start_child({:via, PartitionSupervisor, {Microwaveprop.TaskSupervisor, self()}}, fn -> + Enum.each(tables, fn t -> + Logger.info("Weather.analyze_all: ANALYZE #{t}") + started = System.monotonic_time(:millisecond) - try do - Repo.query!("ANALYZE #{quote_ident(t)}", [], timeout: :infinity) - elapsed = System.monotonic_time(:millisecond) - started - Logger.info("Weather.analyze_all: ANALYZE #{t} done in #{elapsed}ms") - rescue - e -> Logger.warning("Weather.analyze_all: ANALYZE #{t} failed: #{inspect(e)}") - end + try do + _ = Repo.query!("ANALYZE #{quote_ident(t)}", [], timeout: :infinity) + elapsed = System.monotonic_time(:millisecond) - started + Logger.info("Weather.analyze_all: ANALYZE #{t} done in #{elapsed}ms") + rescue + e -> Logger.warning("Weather.analyze_all: ANALYZE #{t} failed: #{inspect(e)}") + end + end) + + Logger.info("Weather.analyze_all: complete (#{length(tables)} tables)") end) - Logger.info("Weather.analyze_all: complete (#{length(tables)} tables)") - end) - %{queued: length(tables), tables: tables} end @@ -539,43 +540,44 @@ defmodule Microwaveprop.Weather do now = DateTime.truncate(DateTime.utc_now(), :second) - if updates != [] do - {values_sql, params} = - updates - |> Enum.with_index() - |> Enum.reduce({"", []}, fn u, {sql, params} -> - base = Enum.count(params) - frag = "($#{base + 1}::uuid, $#{base + 2}, $#{base + 3}, $#{base + 4}, $#{base + 5}, $#{base + 6})" - sep = if sql == "", do: "", else: ", " + _ = + if updates != [] do + {values_sql, params} = + updates + |> Enum.with_index() + |> Enum.reduce({"", []}, fn u, {sql, params} -> + base = Enum.count(params) + frag = "($#{base + 1}::uuid, $#{base + 2}, $#{base + 3}, $#{base + 4}, $#{base + 5}, $#{base + 6})" + sep = if sql == "", do: "", else: ", " - params = - params ++ - [ - Ecto.UUID.dump!(u.id), - u.surface_refractivity, - u.min_refractivity_gradient, - u.ducting_detected, - u.duct_characteristics || [], - now - ] + params = + params ++ + [ + Ecto.UUID.dump!(u.id), + u.surface_refractivity, + u.min_refractivity_gradient, + u.ducting_detected, + u.duct_characteristics || [], + now + ] - {sql <> sep <> frag, params} - end) + {sql <> sep <> frag, params} + end) - Repo.query!( - "UPDATE hrrr_profiles AS h " <> - "SET surface_refractivity = v.surface_refractivity, " <> - "min_refractivity_gradient = v.min_refractivity_gradient, " <> - "ducting_detected = v.ducting_detected, " <> - "duct_characteristics = v.duct_characteristics, " <> - "updated_at = v.updated_at " <> - "FROM (VALUES #{values_sql}) " <> - "AS v(id, surface_refractivity, min_refractivity_gradient, " <> - "ducting_detected, duct_characteristics, updated_at) " <> - "WHERE h.id = v.id", - params - ) - end + Repo.query!( + "UPDATE hrrr_profiles AS h " <> + "SET surface_refractivity = v.surface_refractivity, " <> + "min_refractivity_gradient = v.min_refractivity_gradient, " <> + "ducting_detected = v.ducting_detected, " <> + "duct_characteristics = v.duct_characteristics, " <> + "updated_at = v.updated_at " <> + "FROM (VALUES #{values_sql}) " <> + "AS v(id, surface_refractivity, min_refractivity_gradient, " <> + "ducting_detected, duct_characteristics, updated_at) " <> + "WHERE h.id = v.id", + params + ) + end length(updates) end diff --git a/lib/microwaveprop/weather/grib2/wgrib2.ex b/lib/microwaveprop/weather/grib2/wgrib2.ex index dcb5896c..3e1e996b 100644 --- a/lib/microwaveprop/weather/grib2/wgrib2.ex +++ b/lib/microwaveprop/weather/grib2/wgrib2.ex @@ -657,7 +657,7 @@ defmodule Microwaveprop.Weather.Grib2.Wgrib2 do # 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 # `.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 case Regex.run(~r/lon=([\d.]+),lat=([\d.]+),val=([\d.eE+-]+)/, segment) do [_, lon_str, lat_str, val_str] -> diff --git a/lib/microwaveprop/weather/grid_cache.ex b/lib/microwaveprop/weather/grid_cache.ex index 37b26601..45918f14 100644 --- a/lib/microwaveprop/weather/grid_cache.ex +++ b/lib/microwaveprop/weather/grid_cache.ex @@ -184,7 +184,7 @@ defmodule Microwaveprop.Weather.GridCache do def handle_call(:sync, _from, state), do: {:reply, :ok, state} 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]) :ets.delete_all_objects(@lock_table) @@ -291,10 +291,14 @@ defmodule Microwaveprop.Weather.GridCache do end defp ets_latest_valid_time do - :ets.foldl(fn - {vt, _}, nil -> vt - {vt, _}, latest -> if DateTime.compare(vt, latest) == :gt, do: vt, else: latest - end, nil, @table) + :ets.foldl( + fn + {vt, _}, nil -> vt + {vt, _}, latest -> if DateTime.after?(vt, latest), do: vt, else: latest + end, + nil, + @table + ) end defp ets_prune_keep_latest(keep) do @@ -475,12 +479,13 @@ defmodule Microwaveprop.Weather.GridCache do end defp valkey_clear do - case Valkey.scan_match("#{@valkey_chunk_prefix}*") do - {:ok, keys} -> Valkey.del(keys) - _ -> :ok - end + _ = + case Valkey.scan_match("#{@valkey_chunk_prefix}*") do + {:ok, keys} -> _ = Valkey.del(keys) + _ -> :ok + end - Valkey.del([@valkey_index_key]) + _ = Valkey.del([@valkey_index_key]) end defp chunk_key_pattern(valid_time) do diff --git a/lib/microwaveprop/weather/nexrad_cache.ex b/lib/microwaveprop/weather/nexrad_cache.ex index b6350076..d2f27a9e 100644 --- a/lib/microwaveprop/weather/nexrad_cache.ex +++ b/lib/microwaveprop/weather/nexrad_cache.ex @@ -57,10 +57,14 @@ defmodule Microwaveprop.Weather.NexradCache do # 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. defp oldest_key do - :ets.foldl(fn - {ts, _, _}, nil -> ts - {ts, _, _}, oldest -> if DateTime.before?(ts, oldest), do: ts, else: oldest - end, nil, @table) + :ets.foldl( + fn + {ts, _, _}, nil -> ts + {ts, _, _}, oldest -> if DateTime.before?(ts, oldest), do: ts, else: oldest + end, + nil, + @table + ) end @spec prune_older_than(DateTime.t()) :: non_neg_integer() diff --git a/lib/microwaveprop/weather/scalar_file.ex b/lib/microwaveprop/weather/scalar_file.ex index de1ff8ad..8711cef9 100644 --- a/lib/microwaveprop/weather/scalar_file.ex +++ b/lib/microwaveprop/weather/scalar_file.ex @@ -134,9 +134,7 @@ defmodule Microwaveprop.Weather.ScalarFile do # leave stale chunks behind. case File.ls(dir) do {:ok, entries} -> - for entry <- entries do - _ = File.rm(Path.join(dir, entry)) - end + Enum.each(entries, &File.rm!(Path.join(dir, &1))) _ -> :ok diff --git a/lib/microwaveprop/workers/rover_path_profile_worker.ex b/lib/microwaveprop/workers/rover_path_profile_worker.ex index c008dd7e..e5f90152 100644 --- a/lib/microwaveprop/workers/rover_path_profile_worker.ex +++ b/lib/microwaveprop/workers/rover_path_profile_worker.ex @@ -109,7 +109,7 @@ defmodule Microwaveprop.Workers.RoverPathProfileWorker do rescue 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__ end @@ -128,7 +128,7 @@ defmodule Microwaveprop.Workers.RoverPathProfileWorker do |> Repo.update() |> case do {:ok, updated} -> - broadcast(updated) + _ = broadcast(updated) :ok {:error, cs} -> @@ -216,7 +216,7 @@ defmodule Microwaveprop.Workers.RoverPathProfileWorker do |> Repo.update() |> case do {:ok, updated} -> - broadcast(updated) + _ = broadcast(updated) {:error, message} {:error, cs} -> diff --git a/lib/microwaveprop_web/api/rate_limiter.ex b/lib/microwaveprop_web/api/rate_limiter.ex index c8bdb750..5ec6718c 100644 --- a/lib/microwaveprop_web/api/rate_limiter.ex +++ b/lib/microwaveprop_web/api/rate_limiter.ex @@ -50,13 +50,14 @@ defmodule MicrowavepropWeb.Api.RateLimiter do """ @spec init_table() :: :ok def init_table do - if :ets.whereis(@table) == :undefined do - try do - :ets.new(@table, [:set, :public, :named_table, write_concurrency: true]) - rescue - ArgumentError -> :ok + _ = + if :ets.whereis(@table) == :undefined do + try do + :ets.new(@table, [:set, :public, :named_table, write_concurrency: true]) + rescue + ArgumentError -> :ok + end end - end :ok end diff --git a/lib/microwaveprop_web/controllers/scores_controller.ex b/lib/microwaveprop_web/controllers/scores_controller.ex index 070a2d82..9a11fd6e 100644 --- a/lib/microwaveprop_web/controllers/scores_controller.ex +++ b/lib/microwaveprop_web/controllers/scores_controller.ex @@ -105,13 +105,12 @@ defmodule MicrowavepropWeb.ScoresController do {lats, lons, score_bytes} = Enum.reduce(scores, {[], [], []}, fn s, {lats_acc, lons_acc, scs_acc} -> - {[<> | lats_acc], - [<> | lons_acc], + {[<> | lats_acc], [<> | lons_acc], [<> | scs_acc]} end) - <> + <> end defp clamp_score(s) when is_integer(s) and s < 0, do: 0 diff --git a/lib/microwaveprop_web/live/contact_live/index.ex b/lib/microwaveprop_web/live/contact_live/index.ex index 0fa79b3e..781bba9c 100644 --- a/lib/microwaveprop_web/live/contact_live/index.ex +++ b/lib/microwaveprop_web/live/contact_live/index.ex @@ -95,22 +95,22 @@ defmodule MicrowavepropWeb.ContactLive.Index do |> Repo.all() |> Map.new() - 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() + 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() - Enum.map(counts, fn {month, count} -> - x = (month - 1) * @chart_bar_pitch + @chart_bar_offset - height = bar_height(count, max_count) + Enum.map(counts, fn {month, count} -> + x = (month - 1) * @chart_bar_pitch + @chart_bar_offset + height = bar_height(count, max_count) - %{ - month: month, - label: Enum.at(@month_labels, month - 1), - count: count, - x: x, - y: @chart_baseline - height, - height: height - } - end) + %{ + month: month, + label: Enum.at(@month_labels, month - 1), + count: count, + x: x, + y: @chart_baseline - height, + height: height + } + end) end) end diff --git a/lib/microwaveprop_web/live/contact_live/show.ex b/lib/microwaveprop_web/live/contact_live/show.ex index 8571802e..46193c25 100644 --- a/lib/microwaveprop_web/live/contact_live/show.ex +++ b/lib/microwaveprop_web/live/contact_live/show.ex @@ -225,7 +225,7 @@ defmodule MicrowavepropWeb.ContactLive.Show do def handle_event("delete_contact", _params, socket) do if admin?(socket.assigns) do - Radio.delete_contact!(socket.assigns.contact) + _ = Radio.delete_contact!(socket.assigns.contact) {:noreply, socket diff --git a/lib/microwaveprop_web/live/rover_planning_live/show.ex b/lib/microwaveprop_web/live/rover_planning_live/show.ex index ddd99dcb..82fc10cf 100644 --- a/lib/microwaveprop_web/live/rover_planning_live/show.ex +++ b/lib/microwaveprop_web/live/rover_planning_live/show.ex @@ -24,9 +24,10 @@ defmodule MicrowavepropWeb.RoverPlanningLive.Show do |> push_navigate(to: ~p"/rover-planning")} %Mission{} = mission -> - if connected?(socket) do - Phoenix.PubSub.subscribe(Microwaveprop.PubSub, "rover_planning:#{mission.id}") - end + _ = + if connected?(socket) do + Phoenix.PubSub.subscribe(Microwaveprop.PubSub, "rover_planning:#{mission.id}") + end {:ok, assign(socket, diff --git a/lib/microwaveprop_web/live/skewt_location_resolver.ex b/lib/microwaveprop_web/live/skewt_location_resolver.ex index e0c67f80..56cbb14b 100644 --- a/lib/microwaveprop_web/live/skewt_location_resolver.ex +++ b/lib/microwaveprop_web/live/skewt_location_resolver.ex @@ -70,9 +70,6 @@ defmodule MicrowavepropWeb.SkewtLocationResolver do {:error, reason} when is_binary(reason) -> {:error, "callsign lookup failed: #{reason}"} - - {:error, _} -> - {:error, "callsign lookup failed"} end end @@ -83,9 +80,6 @@ defmodule MicrowavepropWeb.SkewtLocationResolver do {:error, reason} when is_binary(reason) -> {:error, "geocode failed: #{reason}"} - - {:error, _} -> - {:error, "geocode failed"} end end end diff --git a/lib/microwaveprop_web/live/skewt_svg.ex b/lib/microwaveprop_web/live/skewt_svg.ex index a8e61beb..f533863b 100644 --- a/lib/microwaveprop_web/live/skewt_svg.ex +++ b/lib/microwaveprop_web/live/skewt_svg.ex @@ -47,7 +47,7 @@ defmodule MicrowavepropWeb.SkewtSvg do ascent line is overlaid on the temperature trace, matching the 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 cleaned = clean_profile(profile) parcel = Keyword.get(opts, :parcel_trace, []) diff --git a/lib/mix/tasks/calibrate.aprs_144.ex b/lib/mix/tasks/calibrate.aprs_144.ex index acd27617..a5862083 100644 --- a/lib/mix/tasks/calibrate.aprs_144.ex +++ b/lib/mix/tasks/calibrate.aprs_144.ex @@ -220,5 +220,4 @@ defmodule Mix.Tasks.Calibrate.Aprs144 do end 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 diff --git a/lib/mix/tasks/unused.ex b/lib/mix/tasks/unused.ex index 6fe6844e..00b39d0a 100644 --- a/lib/mix/tasks/unused.ex +++ b/lib/mix/tasks/unused.ex @@ -40,6 +40,8 @@ defmodule Mix.Tasks.Unused do use Mix.Task + @dialyzer {:no_opaque, run: 1} + # Callbacks that are invoked by the runtime, not by direct function calls. # They look unused to static analysis but are actually called by OTP / # Phoenix / LiveView machinery. @@ -376,6 +378,7 @@ defmodule Mix.Tasks.Unused do end end + @spec analyze_beam(String.t()) :: {MapSet.t(), MapSet.t()} defp analyze_beam(beam_file) do module = beam_module(beam_file) @@ -388,7 +391,7 @@ defmodule Mix.Tasks.Unused do {defs, calls} - {:error, _reason} -> + _ -> # Binary module or unreadable — skip {MapSet.new(), MapSet.new()} end diff --git a/mix.lock b/mix.lock index 233b990d..26790b2b 100644 --- a/mix.lock +++ b/mix.lock @@ -82,7 +82,6 @@ "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"}, "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"}, "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"}, diff --git a/priv/repo/migrations/20260601202826_add_performance_indexes.exs b/priv/repo/migrations/20260601202826_add_performance_indexes.exs index f93096e2..b11aaf71 100644 --- a/priv/repo/migrations/20260601202826_add_performance_indexes.exs +++ b/priv/repo/migrations/20260601202826_add_performance_indexes.exs @@ -13,26 +13,26 @@ defmodule Microwaveprop.Repo.Migrations.AddPerformanceIndexes do # hrrr_profiles.surface_refractivity IS NULL — backfill query scans for nulls create index(:hrrr_profiles, [:surface_refractivity], - where: "surface_refractivity IS NULL", - name: "hrrr_profiles_surface_refractivity_null_idx" - ) + where: "surface_refractivity IS NULL", + name: "hrrr_profiles_surface_refractivity_null_idx" + ) # hrrr_native_profiles.bulk_richardson IS NULL — derive task scans for nulls create index(:hrrr_native_profiles, [:bulk_richardson], - where: "bulk_richardson IS NULL AND level_count > 2", - name: "hrrr_native_profiles_bulk_richardson_null_idx" - ) + where: "bulk_richardson IS NULL AND level_count > 2", + name: "hrrr_native_profiles_bulk_richardson_null_idx" + ) # grid_tasks.claimed_at for status='running' — reclaim-stale query create index(:grid_tasks, [:claimed_at], - where: "status = 'running'", - name: "grid_tasks_running_claimed_at_idx" - ) + where: "status = 'running'", + name: "grid_tasks_running_claimed_at_idx" + ) # rover_mission_paths composite — worker lookup on all 4 columns 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 create index(:fixed_stations, [:user_id, :position, :inserted_at])