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:
parent
aa5c2b4808
commit
d67186176f
38 changed files with 235 additions and 224 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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"),
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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 """
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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])
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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])
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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: ""
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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] ->
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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} ->
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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, [])
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
1
mix.lock
1
mix.lock
|
|
@ -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"},
|
||||||
|
|
|
||||||
|
|
@ -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])
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue