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
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

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
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"),

View file

@ -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

View file

@ -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 """

View file

@ -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

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
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])

View file

@ -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

View file

@ -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])

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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
)

View file

@ -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

View file

@ -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

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({: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

View file

@ -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

View file

@ -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: ""

View file

@ -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

View file

@ -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

View file

@ -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

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)
# 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] ->

View file

@ -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

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
# 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()

View file

@ -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

View file

@ -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} ->

View file

@ -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

View file

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

View file

@ -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

View file

@ -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,

View file

@ -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

View file

@ -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, [])

View file

@ -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

View file

@ -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

View file

@ -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"},

View file

@ -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])