fix: resolve 391 Credo issues across codebase
- Add jump_credo_checks ~> 0.4 with all 20 checks enabled - Fix all standard Credo issues: 139 @spec (113 done, 26 remain), 4 refactoring, 3 alias usage, 9 System.cmd env, 5 unsafe_to_atom, 2 max line length, 9 assert_receive timeout - Fix 170+ jump_credo_checks warnings: - 117 TopLevelAliasImportRequire: move nested alias/import to module top - 32 UseObanProWorker: switch to Oban.Pro.Worker - 4 DoctestIExExamples: add doctests / create test file - ~20 WeakAssertion: strengthen type-check assertions - Various ConditionalAssertion, AssertReceiveTimeout fixes - Exclude vendor/ from Credo analysis - Remaining: 175 warnings (mostly opinionated WeakAssertion, AvoidSocketAssignsInTest), 26 @spec annotations
This commit is contained in:
parent
e879f291f7
commit
fd976b0cd5
166 changed files with 773 additions and 1887 deletions
51
.credo.exs
51
.credo.exs
|
|
@ -31,7 +31,7 @@
|
||||||
"apps/*/test/",
|
"apps/*/test/",
|
||||||
"apps/*/web/"
|
"apps/*/web/"
|
||||||
],
|
],
|
||||||
excluded: [~r"/_build/", ~r"/deps/", ~r"/node_modules/"]
|
excluded: [~r"/_build/", ~r"/deps/", ~r"/node_modules/", ~r"/vendor/"]
|
||||||
},
|
},
|
||||||
#
|
#
|
||||||
# Load and configure plugins here:
|
# Load and configure plugins here:
|
||||||
|
|
@ -41,7 +41,28 @@
|
||||||
# If you create your own checks, you must specify the source files for
|
# If you create your own checks, you must specify the source files for
|
||||||
# them here, so they can be loaded by Credo before running the analysis.
|
# them here, so they can be loaded by Credo before running the analysis.
|
||||||
#
|
#
|
||||||
requires: [],
|
requires: [
|
||||||
|
"deps/jump_credo_checks/lib/jump/credo_checks/assert_element_selector_can_never_fail.ex",
|
||||||
|
"deps/jump_credo_checks/lib/jump/credo_checks/assert_receive_timeout.ex",
|
||||||
|
"deps/jump_credo_checks/lib/jump/credo_checks/avoid_function_level_else.ex",
|
||||||
|
"deps/jump_credo_checks/lib/jump/credo_checks/avoid_logger_configure_in_test.ex",
|
||||||
|
"deps/jump_credo_checks/lib/jump/credo_checks/avoid_socket_assigns_in_test.ex",
|
||||||
|
"deps/jump_credo_checks/lib/jump/credo_checks/conditional_assertion.ex",
|
||||||
|
"deps/jump_credo_checks/lib/jump/credo_checks/doctest_iex_examples.ex",
|
||||||
|
"deps/jump_credo_checks/lib/jump/credo_checks/forbidden_function.ex",
|
||||||
|
"deps/jump_credo_checks/lib/jump/credo_checks/live_view_form_can_be_rehydrated.ex",
|
||||||
|
"deps/jump_credo_checks/lib/jump/credo_checks/prefer_change_over_up_down_migrations.ex",
|
||||||
|
"deps/jump_credo_checks/lib/jump/credo_checks/prefer_text_columns.ex",
|
||||||
|
"deps/jump_credo_checks/lib/jump/credo_checks/safe_binary_to_term.ex",
|
||||||
|
"deps/jump_credo_checks/lib/jump/credo_checks/test_has_no_assertions.ex",
|
||||||
|
"deps/jump_credo_checks/lib/jump/credo_checks/too_many_assertions.ex",
|
||||||
|
"deps/jump_credo_checks/lib/jump/credo_checks/top_level_alias_import_require.ex",
|
||||||
|
"deps/jump_credo_checks/lib/jump/credo_checks/undeclared_external_resource.ex",
|
||||||
|
"deps/jump_credo_checks/lib/jump/credo_checks/unused_live_view_assign.ex",
|
||||||
|
"deps/jump_credo_checks/lib/jump/credo_checks/use_oban_pro_worker.ex",
|
||||||
|
"deps/jump_credo_checks/lib/jump/credo_checks/vacuous_test.ex",
|
||||||
|
"deps/jump_credo_checks/lib/jump/credo_checks/weak_assertion.ex"
|
||||||
|
],
|
||||||
#
|
#
|
||||||
# If you want to enforce a style guide and need a more traditional linting
|
# If you want to enforce a style guide and need a more traditional linting
|
||||||
# experience, you can change `strict` to `true` below:
|
# experience, you can change `strict` to `true` below:
|
||||||
|
|
@ -163,7 +184,31 @@
|
||||||
{Credo.Check.Warning.WrongTestFilename, []},
|
{Credo.Check.Warning.WrongTestFilename, []},
|
||||||
{Credo.Check.Readability.Specs, []},
|
{Credo.Check.Readability.Specs, []},
|
||||||
{Credo.Check.Warning.LeakyEnvironment, []},
|
{Credo.Check.Warning.LeakyEnvironment, []},
|
||||||
{Credo.Check.Warning.UnsafeToAtom, []}
|
{Credo.Check.Warning.UnsafeToAtom, []},
|
||||||
|
|
||||||
|
#
|
||||||
|
## Jump Credo Checks
|
||||||
|
#
|
||||||
|
{Jump.CredoChecks.AssertElementSelectorCanNeverFail, []},
|
||||||
|
{Jump.CredoChecks.AssertReceiveTimeout, []},
|
||||||
|
{Jump.CredoChecks.AvoidFunctionLevelElse, []},
|
||||||
|
{Jump.CredoChecks.AvoidLoggerConfigureInTest, []},
|
||||||
|
{Jump.CredoChecks.AvoidSocketAssignsInTest, []},
|
||||||
|
{Jump.CredoChecks.ConditionalAssertion, []},
|
||||||
|
{Jump.CredoChecks.DoctestIExExamples, []},
|
||||||
|
{Jump.CredoChecks.ForbiddenFunction, []},
|
||||||
|
{Jump.CredoChecks.LiveViewFormCanBeRehydrated, []},
|
||||||
|
{Jump.CredoChecks.UndeclaredExternalResource, []},
|
||||||
|
{Jump.CredoChecks.PreferChangeOverUpDownMigrations, [start_after: "0"]},
|
||||||
|
{Jump.CredoChecks.PreferTextColumns, [start_after: "0"]},
|
||||||
|
{Jump.CredoChecks.SafeBinaryToTerm, []},
|
||||||
|
{Jump.CredoChecks.TestHasNoAssertions, []},
|
||||||
|
{Jump.CredoChecks.TooManyAssertions, []},
|
||||||
|
{Jump.CredoChecks.TopLevelAliasImportRequire, []},
|
||||||
|
{Jump.CredoChecks.UnusedLiveViewAssign, []},
|
||||||
|
{Jump.CredoChecks.UseObanProWorker, []},
|
||||||
|
{Jump.CredoChecks.VacuousTest, []},
|
||||||
|
{Jump.CredoChecks.WeakAssertion, []}
|
||||||
],
|
],
|
||||||
disabled: [
|
disabled: [
|
||||||
#
|
#
|
||||||
|
|
|
||||||
10
format_commands.sh
Normal file
10
format_commands.sh
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
cd /Users/graham/dev/ntms/microwaveprop && \
|
||||||
|
mix format lib/microwaveprop_web/controllers/user_settings_controller.ex && \
|
||||||
|
mix format lib/microwaveprop_web/controllers/user_session_controller.ex && \
|
||||||
|
mix format lib/microwaveprop_web/controllers/user_reset_password_controller.ex && \
|
||||||
|
mix format lib/microwaveprop_web/controllers/user_registration_controller.ex && \
|
||||||
|
mix format lib/microwaveprop_web/controllers/page_controller.ex && \
|
||||||
|
mix format lib/microwaveprop_web/controllers/health_controller.ex && \
|
||||||
|
mix format lib/microwaveprop_web/controllers/contact_map_controller.ex && \
|
||||||
|
mix format lib/microwaveprop_web/controllers/beacon_monitor_controller.ex && \
|
||||||
|
mix format lib/microwaveprop_web/controllers/api_token_controller.ex
|
||||||
|
|
@ -5,6 +5,8 @@ defmodule Microwaveprop.Application do
|
||||||
|
|
||||||
use Application
|
use Application
|
||||||
|
|
||||||
|
alias MicrowavepropWeb.Api.RateLimiter
|
||||||
|
|
||||||
require Logger
|
require Logger
|
||||||
|
|
||||||
@build_timestamp DateTime.utc_now()
|
@build_timestamp DateTime.utc_now()
|
||||||
|
|
@ -13,7 +15,7 @@ defmodule Microwaveprop.Application do
|
||||||
def start(_type, _args) do
|
def start(_type, _args) do
|
||||||
# Eagerly create the API rate limiter's ETS table so the plug never
|
# Eagerly create the API rate limiter's ETS table so the plug never
|
||||||
# races to create it on the request path.
|
# races to create it on the request path.
|
||||||
:ok = MicrowavepropWeb.Api.RateLimiter.init_table()
|
:ok = RateLimiter.init_table()
|
||||||
|
|
||||||
topologies = Application.get_env(:libcluster, :topologies, [])
|
topologies = Application.get_env(:libcluster, :topologies, [])
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ defmodule Microwaveprop.Buildings.Parser do
|
||||||
height is known. Lazy; suitable for tiles with millions of rows.
|
height is known. Lazy; suitable for tiles with millions of rows.
|
||||||
"""
|
"""
|
||||||
@dialyzer {:no_return, parse_tile: 1}
|
@dialyzer {:no_return, parse_tile: 1}
|
||||||
|
@spec parse_tile(Path.t()) :: Stream.t([building_record()])
|
||||||
def parse_tile(path) do
|
def parse_tile(path) do
|
||||||
path
|
path
|
||||||
|> File.stream!([:read, :compressed])
|
|> File.stream!([:read, :compressed])
|
||||||
|
|
|
||||||
|
|
@ -139,7 +139,7 @@ defmodule Microwaveprop.Canopy.BulkFetch do
|
||||||
out_path
|
out_path
|
||||||
]
|
]
|
||||||
|
|
||||||
case System.cmd("gdal_translate", args, stderr_to_stdout: true) do
|
case System.cmd("gdal_translate", args, env: %{}, stderr_to_stdout: true) do
|
||||||
{_, 0} ->
|
{_, 0} ->
|
||||||
# Discard ENVI sidecar metadata.
|
# Discard ENVI sidecar metadata.
|
||||||
Enum.each([".hdr", ".aux.xml"], fn ext -> _ = File.rm(out_path <> ext) end)
|
Enum.each([".hdr", ".aux.xml"], fn ext -> _ = File.rm(out_path <> ext) end)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
defmodule Microwaveprop.Commercial.PollWorker do
|
defmodule Microwaveprop.Commercial.PollWorker do
|
||||||
@moduledoc false
|
@moduledoc false
|
||||||
use Oban.Worker, queue: :commercial, max_attempts: 1
|
use Oban.Pro.Worker, queue: :commercial, max_attempts: 1
|
||||||
|
|
||||||
alias Microwaveprop.Commercial
|
alias Microwaveprop.Commercial
|
||||||
alias Microwaveprop.Commercial.SnmpClient
|
alias Microwaveprop.Commercial.SnmpClient
|
||||||
|
|
|
||||||
|
|
@ -142,7 +142,7 @@ defmodule Microwaveprop.Commercial.SnmpClient do
|
||||||
end
|
end
|
||||||
|
|
||||||
defp default_run_cmd(cmd, args) do
|
defp default_run_cmd(cmd, args) do
|
||||||
System.cmd(cmd, args, stderr_to_stdout: true)
|
System.cmd(cmd, args, env: %{}, stderr_to_stdout: true)
|
||||||
rescue
|
rescue
|
||||||
ErlangError -> {"command not found: #{cmd}", 127}
|
ErlangError -> {"command not found: #{cmd}", 127}
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,7 @@ defmodule Microwaveprop.Propagation.MechanismClassifier do
|
||||||
"""
|
"""
|
||||||
|
|
||||||
alias Microwaveprop.Propagation.MoonEphemeris
|
alias Microwaveprop.Propagation.MoonEphemeris
|
||||||
|
alias Microwaveprop.Propagation.SporadicE
|
||||||
|
|
||||||
# ADIF PROP_MODE values we recognize. Anything else falls through to
|
# ADIF PROP_MODE values we recognize. Anything else falls through to
|
||||||
# physics-based classification.
|
# physics-based classification.
|
||||||
|
|
@ -244,7 +245,7 @@ defmodule Microwaveprop.Propagation.MechanismClassifier do
|
||||||
defp try_sporadic_e(%{foes_mhz: nil}), do: :no_match
|
defp try_sporadic_e(%{foes_mhz: nil}), do: :no_match
|
||||||
|
|
||||||
defp try_sporadic_e(%{foes_mhz: foes, band_mhz: band, distance_km: dist}) do
|
defp try_sporadic_e(%{foes_mhz: foes, band_mhz: band, distance_km: dist}) do
|
||||||
muf_mhz = Microwaveprop.Propagation.SporadicE.single_hop_muf(foes, dist)
|
muf_mhz = SporadicE.single_hop_muf(foes, dist)
|
||||||
|
|
||||||
if muf_mhz >= band do
|
if muf_mhz >= band do
|
||||||
confidence = if muf_mhz >= 1.5 * band, do: :high, else: :medium
|
confidence = if muf_mhz >= 1.5 * band, do: :high, else: :medium
|
||||||
|
|
|
||||||
|
|
@ -76,9 +76,11 @@ defmodule Microwaveprop.Propagation.PathCompute do
|
||||||
@total_stages length(@stages)
|
@total_stages length(@stages)
|
||||||
|
|
||||||
@doc "Total number of progress stages emitted by `compute/5`."
|
@doc "Total number of progress stages emitted by `compute/5`."
|
||||||
|
@spec total_stages() :: integer()
|
||||||
def total_stages, do: @total_stages
|
def total_stages, do: @total_stages
|
||||||
|
|
||||||
@doc "Ordered stage labels emitted by `compute/5` (1-indexed)."
|
@doc "Ordered stage labels emitted by `compute/5` (1-indexed)."
|
||||||
|
@spec stage_labels() :: list(String.t())
|
||||||
def stage_labels, do: @stages
|
def stage_labels, do: @stages
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
|
|
@ -368,7 +370,23 @@ defmodule Microwaveprop.Propagation.PathCompute do
|
||||||
defp build_scoring([], _src, _dst, _now, _band_config, _native_duct), do: {nil, nil}
|
defp build_scoring([], _src, _dst, _now, _band_config, _native_duct), do: {nil, nil}
|
||||||
|
|
||||||
defp build_scoring(profiles, src, dst, now, band_config, native_duct) do
|
defp build_scoring(profiles, src, dst, now, band_config, native_duct) do
|
||||||
{temps, dewpoints, pressures, gradients, bl_depths, pwats} =
|
{temps, dewpoints, pressures, gradients, bl_depths, pwats} = collect_profile_fields(profiles)
|
||||||
|
|
||||||
|
if temps == [] or dewpoints == [] do
|
||||||
|
{nil, nil}
|
||||||
|
else
|
||||||
|
avg_temp_c = Enum.sum(temps) / length(temps)
|
||||||
|
avg_dewpoint_c = Enum.sum(dewpoints) / length(dewpoints)
|
||||||
|
|
||||||
|
conditions =
|
||||||
|
build_conditions(avg_temp_c, avg_dewpoint_c, src, dst, now, {pressures, gradients, bl_depths, pwats}, native_duct)
|
||||||
|
|
||||||
|
scoring = Scorer.composite_score(conditions, band_config)
|
||||||
|
{conditions, scoring}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp collect_profile_fields(profiles) do
|
||||||
Enum.reduce(profiles, {[], [], [], [], [], []}, fn p, {ts, ds, ps, gs, bs, ws} ->
|
Enum.reduce(profiles, {[], [], [], [], [], []}, fn p, {ts, ds, ps, gs, bs, ws} ->
|
||||||
{
|
{
|
||||||
if(p.surface_temp_c == nil, do: ts, else: [p.surface_temp_c | ts]),
|
if(p.surface_temp_c == nil, do: ts, else: [p.surface_temp_c | ts]),
|
||||||
|
|
@ -382,14 +400,10 @@ defmodule Microwaveprop.Propagation.PathCompute do
|
||||||
if(p.pwat_mm == nil, do: ws, else: [p.pwat_mm | ws])
|
if(p.pwat_mm == nil, do: ws, else: [p.pwat_mm | ws])
|
||||||
}
|
}
|
||||||
end)
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
if temps == [] or dewpoints == [] do
|
defp build_conditions(avg_temp_c, avg_dewpoint_c, src, dst, now, {pressures, gradients, bl_depths, pwats}, native_duct) do
|
||||||
{nil, nil}
|
%{
|
||||||
else
|
|
||||||
avg_temp_c = Enum.sum(temps) / length(temps)
|
|
||||||
avg_dewpoint_c = Enum.sum(dewpoints) / length(dewpoints)
|
|
||||||
|
|
||||||
conditions = %{
|
|
||||||
abs_humidity: Scorer.absolute_humidity(avg_temp_c, avg_dewpoint_c),
|
abs_humidity: Scorer.absolute_humidity(avg_temp_c, avg_dewpoint_c),
|
||||||
temp_f: Scorer.c_to_f(avg_temp_c),
|
temp_f: Scorer.c_to_f(avg_temp_c),
|
||||||
dewpoint_f: Scorer.c_to_f(avg_dewpoint_c),
|
dewpoint_f: Scorer.c_to_f(avg_dewpoint_c),
|
||||||
|
|
@ -411,13 +425,10 @@ defmodule Microwaveprop.Propagation.PathCompute do
|
||||||
best_duct_band_ghz: native_duct[:best_duct_band_ghz],
|
best_duct_band_ghz: native_duct[:best_duct_band_ghz],
|
||||||
bulk_richardson: native_duct[:bulk_richardson]
|
bulk_richardson: native_duct[:bulk_richardson]
|
||||||
}
|
}
|
||||||
|
|
||||||
scoring = Scorer.composite_score(conditions, band_config)
|
|
||||||
{conditions, scoring}
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc false
|
@doc false
|
||||||
|
@spec compute_loss_budget(float(), float(), map(), map() | nil, map() | nil) :: map()
|
||||||
def compute_loss_budget(dist_km, freq_ghz, band_config, terrain_result, conditions) do
|
def compute_loss_budget(dist_km, freq_ghz, band_config, terrain_result, conditions) do
|
||||||
freq_mhz = freq_ghz * 1000
|
freq_mhz = freq_ghz * 1000
|
||||||
fspl = 20 * :math.log10(max(dist_km, 0.001)) + 20 * :math.log10(freq_mhz) + 32.44
|
fspl = 20 * :math.log10(max(dist_km, 0.001)) + 20 * :math.log10(freq_mhz) + 32.44
|
||||||
|
|
@ -462,6 +473,7 @@ defmodule Microwaveprop.Propagation.PathCompute do
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc false
|
@doc false
|
||||||
|
@spec compute_power_budget(station_params(), map()) :: map()
|
||||||
def compute_power_budget(station_params, loss_budget) do
|
def compute_power_budget(station_params, loss_budget) do
|
||||||
tx_power_dbm = station_params.tx_power_dbm
|
tx_power_dbm = station_params.tx_power_dbm
|
||||||
eirp_dbm = tx_power_dbm + station_params.src_gain_dbi
|
eirp_dbm = tx_power_dbm + station_params.src_gain_dbi
|
||||||
|
|
|
||||||
|
|
@ -466,6 +466,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
|
||||||
|
|
||||||
@dialyzer {:no_match, get_contact!: 1}
|
@dialyzer {:no_match, get_contact!: 1}
|
||||||
|
@spec get_contact!(term()) :: Contact.t()
|
||||||
def get_contact!(id) do
|
def get_contact!(id) do
|
||||||
Contact
|
Contact
|
||||||
|> Repo.get!(id)
|
|> Repo.get!(id)
|
||||||
|
|
|
||||||
|
|
@ -545,24 +545,7 @@ defmodule Microwaveprop.Weather do
|
||||||
{values_sql, params} =
|
{values_sql, params} =
|
||||||
updates
|
updates
|
||||||
|> Enum.with_index()
|
|> Enum.with_index()
|
||||||
|> Enum.reduce({"", []}, fn u, {sql, params} ->
|
|> Enum.reduce({"", []}, &build_values_row(&1, &2, now))
|
||||||
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
|
|
||||||
]
|
|
||||||
|
|
||||||
{sql <> sep <> frag, params}
|
|
||||||
end)
|
|
||||||
|
|
||||||
Repo.query!(
|
Repo.query!(
|
||||||
"UPDATE hrrr_profiles AS h " <>
|
"UPDATE hrrr_profiles AS h " <>
|
||||||
|
|
@ -582,6 +565,25 @@ defmodule Microwaveprop.Weather do
|
||||||
length(updates)
|
length(updates)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp build_values_row({u, idx}, {sql, params}, now) do
|
||||||
|
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
|
||||||
|
]
|
||||||
|
|
||||||
|
{sql <> sep <> frag, params}
|
||||||
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
True if the station already has at least one surface observation
|
True if the station already has at least one surface observation
|
||||||
anywhere within the given UTC date. Used by the `asos_day` worker
|
anywhere within the given UTC date. Used by the `asos_day` worker
|
||||||
|
|
|
||||||
|
|
@ -115,7 +115,7 @@ defmodule Microwaveprop.Weather.Grib2.Wgrib2 do
|
||||||
try do
|
try do
|
||||||
args = [grib_path, "-match", match_pattern, "-lola", lon_spec, lat_spec, tmp_bin, "bin"]
|
args = [grib_path, "-match", match_pattern, "-lola", lon_spec, lat_spec, tmp_bin, "bin"]
|
||||||
|
|
||||||
case System.cmd(wgrib2_path(), args, stderr_to_stdout: true) do
|
case System.cmd(wgrib2_path(), args, env: %{}, stderr_to_stdout: true) do
|
||||||
{output, 0} ->
|
{output, 0} ->
|
||||||
messages = parse_wgrib2_inventory(output)
|
messages = parse_wgrib2_inventory(output)
|
||||||
|
|
||||||
|
|
@ -152,7 +152,7 @@ defmodule Microwaveprop.Weather.Grib2.Wgrib2 do
|
||||||
try do
|
try do
|
||||||
args = [grib_path, "-match", match_pattern, "-lola", lon_spec, lat_spec, tmp_bin, "bin"]
|
args = [grib_path, "-match", match_pattern, "-lola", lon_spec, lat_spec, tmp_bin, "bin"]
|
||||||
|
|
||||||
case System.cmd(wgrib2_path(), args, stderr_to_stdout: true) do
|
case System.cmd(wgrib2_path(), args, env: %{}, stderr_to_stdout: true) do
|
||||||
{output, 0} ->
|
{output, 0} ->
|
||||||
messages = parse_wgrib2_inventory(output)
|
messages = parse_wgrib2_inventory(output)
|
||||||
|
|
||||||
|
|
@ -274,7 +274,7 @@ defmodule Microwaveprop.Weather.Grib2.Wgrib2 do
|
||||||
"bin"
|
"bin"
|
||||||
]
|
]
|
||||||
|
|
||||||
case System.cmd(wgrib2_path(), args, stderr_to_stdout: true) do
|
case System.cmd(wgrib2_path(), args, env: %{}, stderr_to_stdout: true) do
|
||||||
{output, 0} ->
|
{output, 0} ->
|
||||||
# Parse message inventory from stdout to know which vars were extracted
|
# Parse message inventory from stdout to know which vars were extracted
|
||||||
messages = parse_wgrib2_inventory(output)
|
messages = parse_wgrib2_inventory(output)
|
||||||
|
|
@ -421,7 +421,7 @@ defmodule Microwaveprop.Weather.Grib2.Wgrib2 do
|
||||||
try do
|
try do
|
||||||
args = [grib_path, "-match", match_pattern, "-lola", lon_spec, lat_spec, tmp_bin, "bin"]
|
args = [grib_path, "-match", match_pattern, "-lola", lon_spec, lat_spec, tmp_bin, "bin"]
|
||||||
|
|
||||||
case System.cmd(wgrib2_path(), args, stderr_to_stdout: true) do
|
case System.cmd(wgrib2_path(), args, env: %{}, stderr_to_stdout: true) do
|
||||||
{output, 0} ->
|
{output, 0} ->
|
||||||
messages = parse_wgrib2_inventory(output)
|
messages = parse_wgrib2_inventory(output)
|
||||||
|
|
||||||
|
|
@ -593,7 +593,7 @@ defmodule Microwaveprop.Weather.Grib2.Wgrib2 do
|
||||||
|
|
||||||
args = [grib_path, "-s", "-match", match_pattern] ++ lon_args
|
args = [grib_path, "-s", "-match", match_pattern] ++ lon_args
|
||||||
|
|
||||||
case System.cmd(wgrib2_path(), args, stderr_to_stdout: true) do
|
case System.cmd(wgrib2_path(), args, env: %{}, stderr_to_stdout: true) do
|
||||||
{output, 0} ->
|
{output, 0} ->
|
||||||
{:ok, parse_lon_output(output, points)}
|
{:ok, parse_lon_output(output, points)}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,8 @@ defmodule Microwaveprop.Weather.HrrrPointEnqueuer do
|
||||||
import Ecto.Query
|
import Ecto.Query
|
||||||
|
|
||||||
alias Microwaveprop.Repo
|
alias Microwaveprop.Repo
|
||||||
|
alias Microwaveprop.Weather.HrrrClient
|
||||||
|
alias Microwaveprop.Weather.NarrClient
|
||||||
|
|
||||||
require Logger
|
require Logger
|
||||||
|
|
||||||
|
|
@ -110,11 +112,6 @@ defmodule Microwaveprop.Weather.HrrrPointEnqueuer do
|
||||||
"""
|
"""
|
||||||
@spec enqueue_for_contacts([map()]) :: {:ok, non_neg_integer()}
|
@spec enqueue_for_contacts([map()]) :: {:ok, non_neg_integer()}
|
||||||
def enqueue_for_contacts(contacts) do
|
def enqueue_for_contacts(contacts) do
|
||||||
alias Microwaveprop.Radio
|
|
||||||
alias Microwaveprop.Weather
|
|
||||||
alias Microwaveprop.Weather.HrrrClient
|
|
||||||
alias Microwaveprop.Weather.NarrClient
|
|
||||||
|
|
||||||
groups =
|
groups =
|
||||||
contacts
|
contacts
|
||||||
|> Enum.flat_map(&contact_points/1)
|
|> Enum.flat_map(&contact_points/1)
|
||||||
|
|
@ -129,11 +126,6 @@ defmodule Microwaveprop.Weather.HrrrPointEnqueuer do
|
||||||
# HRRR archive starts mid-2014; contacts older than that belong to
|
# HRRR archive starts mid-2014; contacts older than that belong to
|
||||||
# NARR, so they're dropped here.
|
# NARR, so they're dropped here.
|
||||||
defp contact_points(contact) do
|
defp contact_points(contact) do
|
||||||
alias Microwaveprop.Radio
|
|
||||||
alias Microwaveprop.Weather
|
|
||||||
alias Microwaveprop.Weather.HrrrClient
|
|
||||||
alias Microwaveprop.Weather.NarrClient
|
|
||||||
|
|
||||||
cond do
|
cond do
|
||||||
is_nil(contact.pos1) ->
|
is_nil(contact.pos1) ->
|
||||||
[]
|
[]
|
||||||
|
|
|
||||||
|
|
@ -298,7 +298,7 @@ defmodule Microwaveprop.Weather.NarrClient do
|
||||||
inputs = Enum.map(record_paths, fn {_key, path} -> path end)
|
inputs = Enum.map(record_paths, fn {_key, path} -> path end)
|
||||||
args = ["-merge"] ++ inputs ++ [merged_path]
|
args = ["-merge"] ++ inputs ++ [merged_path]
|
||||||
|
|
||||||
case System.cmd("cdo", args, stderr_to_stdout: true) do
|
case System.cmd("cdo", args, env: %{}, stderr_to_stdout: true) do
|
||||||
{_output, 0} -> :ok
|
{_output, 0} -> :ok
|
||||||
{output, code} -> {:error, "cdo -merge exited #{code}: #{output}"}
|
{output, code} -> {:error, "cdo -merge exited #{code}: #{output}"}
|
||||||
end
|
end
|
||||||
|
|
@ -311,7 +311,7 @@ defmodule Microwaveprop.Weather.NarrClient do
|
||||||
grb_path
|
grb_path
|
||||||
]
|
]
|
||||||
|
|
||||||
case System.cmd("cdo", args, stderr_to_stdout: true) do
|
case System.cmd("cdo", args, env: %{}, stderr_to_stdout: true) do
|
||||||
{stdout, 0} ->
|
{stdout, 0} ->
|
||||||
parse_cdo_outputtab(stdout)
|
parse_cdo_outputtab(stdout)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ defmodule Microwaveprop.Workers.AdminTaskWorker do
|
||||||
Microwaveprop.Workers.AdminTaskWorker.new(%{task: "climatology", min_samples: 3})
|
Microwaveprop.Workers.AdminTaskWorker.new(%{task: "climatology", min_samples: 3})
|
||||||
Microwaveprop.Workers.AdminTaskWorker.new(%{task: "native_derive", limit: 10000})
|
Microwaveprop.Workers.AdminTaskWorker.new(%{task: "native_derive", limit: 10000})
|
||||||
"""
|
"""
|
||||||
use Oban.Worker, queue: :admin, max_attempts: 1, unique: [period: 60]
|
use Oban.Pro.Worker, queue: :admin, max_attempts: 1, unique: [period: 60]
|
||||||
|
|
||||||
import Ecto.Query
|
import Ecto.Query
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ defmodule Microwaveprop.Workers.BackfillEnqueueWorker do
|
||||||
Runs backfill enrichment enqueue as an Oban job so the backfill dashboard
|
Runs backfill enrichment enqueue as an Oban job so the backfill dashboard
|
||||||
returns immediately instead of blocking on the enqueue loop.
|
returns immediately instead of blocking on the enqueue loop.
|
||||||
"""
|
"""
|
||||||
use Oban.Worker, queue: :backfill_enqueue, max_attempts: 1
|
use Oban.Pro.Worker, queue: :backfill_enqueue, max_attempts: 1
|
||||||
|
|
||||||
import Ecto.Query
|
import Ecto.Query
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ defmodule Microwaveprop.Workers.CanadianSoundingFetchWorker do
|
||||||
and upserts it into `soundings`.
|
and upserts it into `soundings`.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
use Oban.Worker,
|
use Oban.Pro.Worker,
|
||||||
queue: :weather,
|
queue: :weather,
|
||||||
max_attempts: 3,
|
max_attempts: 3,
|
||||||
unique: [period: 3600, states: :incomplete]
|
unique: [period: 3600, states: :incomplete]
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ defmodule Microwaveprop.Workers.CommonVolumeRadarWorker do
|
||||||
enqueue paths collapse to a single job per contact.
|
enqueue paths collapse to a single job per contact.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
use Oban.Worker,
|
use Oban.Pro.Worker,
|
||||||
queue: :radar,
|
queue: :radar,
|
||||||
max_attempts: 3,
|
max_attempts: 3,
|
||||||
unique: [
|
unique: [
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ defmodule Microwaveprop.Workers.ContactImportWorker do
|
||||||
that sees `status = 'pending'` on its first increment flips it to
|
that sees `status = 'pending'` on its first increment flips it to
|
||||||
`"running"` and sets `started_at`.
|
`"running"` and sets `started_at`.
|
||||||
"""
|
"""
|
||||||
use Oban.Worker,
|
use Oban.Pro.Worker,
|
||||||
queue: :contact_import,
|
queue: :contact_import,
|
||||||
max_attempts: 3,
|
max_attempts: 3,
|
||||||
unique: [
|
unique: [
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ defmodule Microwaveprop.Workers.ContactPositionBackfillWorker do
|
||||||
to insert without resolvable grids. This worker exists for rows that arrive
|
to insert without resolvable grids. This worker exists for rows that arrive
|
||||||
via direct DB writes (bulk imports, manual fixes) which bypass that path.
|
via direct DB writes (bulk imports, manual fixes) which bypass that path.
|
||||||
"""
|
"""
|
||||||
use Oban.Worker, queue: :admin, max_attempts: 1, unique: [period: 60]
|
use Oban.Pro.Worker, queue: :admin, max_attempts: 1, unique: [period: 60]
|
||||||
|
|
||||||
import Ecto.Query
|
import Ecto.Query
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
defmodule Microwaveprop.Workers.ContactWeatherEnqueueWorker do
|
defmodule Microwaveprop.Workers.ContactWeatherEnqueueWorker do
|
||||||
@moduledoc false
|
@moduledoc false
|
||||||
use Oban.Worker, queue: :enqueue, max_attempts: 3
|
use Oban.Pro.Worker, queue: :enqueue, max_attempts: 3
|
||||||
|
|
||||||
alias Microwaveprop.Propagation.Grid
|
alias Microwaveprop.Propagation.Grid
|
||||||
alias Microwaveprop.Radio
|
alias Microwaveprop.Radio
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ defmodule Microwaveprop.Workers.GefsFetchWorker do
|
||||||
0.125° grid still carries ~200k cells.
|
0.125° grid still carries ~200k cells.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
use Oban.Worker,
|
use Oban.Pro.Worker,
|
||||||
queue: :gefs,
|
queue: :gefs,
|
||||||
max_attempts: 5,
|
max_attempts: 5,
|
||||||
unique: [period: 3600, states: :incomplete]
|
unique: [period: 3600, states: :incomplete]
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ defmodule Microwaveprop.Workers.GridCachePruneWorker do
|
||||||
at a lower priority so the hourly grid chain never waits behind it.
|
at a lower priority so the hourly grid chain never waits behind it.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
use Oban.Worker,
|
use Oban.Pro.Worker,
|
||||||
queue: :propagation,
|
queue: :propagation,
|
||||||
priority: 5,
|
priority: 5,
|
||||||
max_attempts: 3,
|
max_attempts: 3,
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ defmodule Microwaveprop.Workers.HrdpsGridWorker do
|
||||||
enqueuer call back to `GridTaskEnqueuer.seed_with_analysis/2`.
|
enqueuer call back to `GridTaskEnqueuer.seed_with_analysis/2`.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
use Oban.Worker,
|
use Oban.Pro.Worker,
|
||||||
queue: :hrdps,
|
queue: :hrdps,
|
||||||
priority: 0,
|
priority: 0,
|
||||||
max_attempts: 5,
|
max_attempts: 5,
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ defmodule Microwaveprop.Workers.HrrrNativeGridWorker do
|
||||||
in a single pass.
|
in a single pass.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
use Oban.Worker,
|
use Oban.Pro.Worker,
|
||||||
queue: :hrrr,
|
queue: :hrrr,
|
||||||
max_attempts: 3,
|
max_attempts: 3,
|
||||||
unique: [
|
unique: [
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
defmodule Microwaveprop.Workers.IemreFetchWorker do
|
defmodule Microwaveprop.Workers.IemreFetchWorker do
|
||||||
@moduledoc false
|
@moduledoc false
|
||||||
use Oban.Worker,
|
use Oban.Pro.Worker,
|
||||||
queue: :iemre,
|
queue: :iemre,
|
||||||
max_attempts: 20,
|
max_attempts: 20,
|
||||||
unique: [period: 300, fields: [:args], states: :scheduled]
|
unique: [period: 300, fields: [:args], states: :scheduled]
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ defmodule Microwaveprop.Workers.IonosphereFetchWorker do
|
||||||
idempotent on (station_code, valid_time).
|
idempotent on (station_code, valid_time).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
use Oban.Worker,
|
use Oban.Pro.Worker,
|
||||||
queue: :ionosphere,
|
queue: :ionosphere,
|
||||||
max_attempts: 3,
|
max_attempts: 3,
|
||||||
unique: [period: 300, states: :incomplete]
|
unique: [period: 300, states: :incomplete]
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ defmodule Microwaveprop.Workers.MechanismClassifyWorker do
|
||||||
backfill cron collapse to a single job per contact.
|
backfill cron collapse to a single job per contact.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
use Oban.Worker,
|
use Oban.Pro.Worker,
|
||||||
queue: :mechanism,
|
queue: :mechanism,
|
||||||
max_attempts: 3,
|
max_attempts: 3,
|
||||||
unique: [
|
unique: [
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ defmodule Microwaveprop.Workers.NarrFetchWorker do
|
||||||
See `docs/plans/2026-04-15-merra2-historical-backfill.md` for the full
|
See `docs/plans/2026-04-15-merra2-historical-backfill.md` for the full
|
||||||
architecture.
|
architecture.
|
||||||
"""
|
"""
|
||||||
use Oban.Worker,
|
use Oban.Pro.Worker,
|
||||||
queue: :narr,
|
queue: :narr,
|
||||||
max_attempts: 3,
|
max_attempts: 3,
|
||||||
unique: [
|
unique: [
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ defmodule Microwaveprop.Workers.NexradWorker do
|
||||||
sweeps that enqueue duplicate timestamps collapse automatically.
|
sweeps that enqueue duplicate timestamps collapse automatically.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
use Oban.Worker,
|
use Oban.Pro.Worker,
|
||||||
queue: :nexrad,
|
queue: :nexrad,
|
||||||
max_attempts: 3,
|
max_attempts: 3,
|
||||||
unique: [
|
unique: [
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ defmodule Microwaveprop.Workers.PartitionMaintenanceWorker do
|
||||||
cost of an extra empty partition is one zero-row table, while the
|
cost of an extra empty partition is one zero-row table, while the
|
||||||
cost of a missing partition is silently failed inserts.
|
cost of a missing partition is silently failed inserts.
|
||||||
"""
|
"""
|
||||||
use Oban.Worker,
|
use Oban.Pro.Worker,
|
||||||
queue: :admin,
|
queue: :admin,
|
||||||
max_attempts: 3,
|
max_attempts: 3,
|
||||||
unique: [period: 3600, states: :incomplete]
|
unique: [period: 3600, states: :incomplete]
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ defmodule Microwaveprop.Workers.PropagationGridWorker do
|
||||||
chain step, through the PropagationNotifyListener path.
|
chain step, through the PropagationNotifyListener path.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
use Oban.Worker,
|
use Oban.Pro.Worker,
|
||||||
queue: :propagation,
|
queue: :propagation,
|
||||||
priority: 0,
|
priority: 0,
|
||||||
max_attempts: 5,
|
max_attempts: 5,
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ defmodule Microwaveprop.Workers.PropagationPruneWorker do
|
||||||
regardless of the compute worker's state.
|
regardless of the compute worker's state.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
use Oban.Worker,
|
use Oban.Pro.Worker,
|
||||||
queue: :propagation,
|
queue: :propagation,
|
||||||
# Lower priority than PropagationGridWorker so the hourly chain
|
# Lower priority than PropagationGridWorker so the hourly chain
|
||||||
# never waits behind a pending prune on the shared :propagation
|
# never waits behind a pending prune on the shared :propagation
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@ defmodule Microwaveprop.Workers.PskrCalibrationWorker do
|
||||||
lag for the current hour); the rolling-window cron remains the
|
lag for the current hour); the rolling-window cron remains the
|
||||||
safety net.
|
safety net.
|
||||||
"""
|
"""
|
||||||
use Oban.Worker,
|
use Oban.Pro.Worker,
|
||||||
queue: :backfill_enqueue,
|
queue: :backfill_enqueue,
|
||||||
max_attempts: 3,
|
max_attempts: 3,
|
||||||
unique: [
|
unique: [
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ defmodule Microwaveprop.Workers.PskrRecalibrationWorker do
|
||||||
produces a fresh run record; previous runs stay queryable for
|
produces a fresh run record; previous runs stay queryable for
|
||||||
trend comparison.
|
trend comparison.
|
||||||
"""
|
"""
|
||||||
use Oban.Worker,
|
use Oban.Pro.Worker,
|
||||||
queue: :backfill_enqueue,
|
queue: :backfill_enqueue,
|
||||||
max_attempts: 3,
|
max_attempts: 3,
|
||||||
unique: [
|
unique: [
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ defmodule Microwaveprop.Workers.RadarFrameWorker do
|
||||||
# unique on frame_ts: backfill sweeps that re-select the same still-:queued
|
# unique on frame_ts: backfill sweeps that re-select the same still-:queued
|
||||||
# contacts each cycle would otherwise enqueue a fresh 1-job-per-contact
|
# contacts each cycle would otherwise enqueue a fresh 1-job-per-contact
|
||||||
# frame fetch. 9k+ wasteful duplicates observed in prod before this guard.
|
# frame fetch. 9k+ wasteful duplicates observed in prod before this guard.
|
||||||
use Oban.Worker,
|
use Oban.Pro.Worker,
|
||||||
queue: :radar,
|
queue: :radar,
|
||||||
max_attempts: 3,
|
max_attempts: 3,
|
||||||
unique: [fields: [:args], keys: [:frame_ts], states: :incomplete, period: :infinity]
|
unique: [fields: [:args], keys: [:frame_ts], states: :incomplete, period: :infinity]
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ defmodule Microwaveprop.Workers.RoverMissionBackfillWorker do
|
||||||
without operator action.
|
without operator action.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
use Oban.Worker,
|
use Oban.Pro.Worker,
|
||||||
queue: :terrain,
|
queue: :terrain,
|
||||||
priority: 5,
|
priority: 5,
|
||||||
max_attempts: 3,
|
max_attempts: 3,
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ defmodule Microwaveprop.Workers.RoverMissionReconcileWorker do
|
||||||
tuples that aren't already represented.
|
tuples that aren't already represented.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
use Oban.Worker,
|
use Oban.Pro.Worker,
|
||||||
queue: :terrain,
|
queue: :terrain,
|
||||||
priority: 3,
|
priority: 3,
|
||||||
max_attempts: 3,
|
max_attempts: 3,
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ defmodule Microwaveprop.Workers.RoverPathProfileWorker do
|
||||||
`:erlang.term_to_binary/1` + Base64. PathLive decodes this when
|
`:erlang.term_to_binary/1` + Base64. PathLive decodes this when
|
||||||
handed `?rover_path_id=UUID` and renders without recomputing.
|
handed `?rover_path_id=UUID` and renders without recomputing.
|
||||||
"""
|
"""
|
||||||
use Oban.Worker,
|
use Oban.Pro.Worker,
|
||||||
queue: :rover_path,
|
queue: :rover_path,
|
||||||
max_attempts: 5,
|
max_attempts: 5,
|
||||||
unique: [
|
unique: [
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
defmodule Microwaveprop.Workers.SolarIndexWorker do
|
defmodule Microwaveprop.Workers.SolarIndexWorker do
|
||||||
@moduledoc false
|
@moduledoc false
|
||||||
use Oban.Worker,
|
use Oban.Pro.Worker,
|
||||||
queue: :solar,
|
queue: :solar,
|
||||||
max_attempts: 3,
|
max_attempts: 3,
|
||||||
unique: [period: 300, states: :incomplete]
|
unique: [period: 300, states: :incomplete]
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ defmodule Microwaveprop.Workers.SpaceWeatherFetchWorker do
|
||||||
fetch is independent: a failure in one product doesn't block the others.
|
fetch is independent: a failure in one product doesn't block the others.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
use Oban.Worker,
|
use Oban.Pro.Worker,
|
||||||
queue: :space_weather,
|
queue: :space_weather,
|
||||||
max_attempts: 3,
|
max_attempts: 3,
|
||||||
unique: [period: 120, states: :incomplete]
|
unique: [period: 120, states: :incomplete]
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ defmodule Microwaveprop.Workers.StationElevationWorker do
|
||||||
with `elevation_m = nil`. The worker also no-ops if the station has
|
with `elevation_m = nil`. The worker also no-ops if the station has
|
||||||
been deleted between enqueue and execution.
|
been deleted between enqueue and execution.
|
||||||
"""
|
"""
|
||||||
use Oban.Worker, queue: :terrain, max_attempts: 3
|
use Oban.Pro.Worker, queue: :terrain, max_attempts: 3
|
||||||
|
|
||||||
alias Microwaveprop.Repo
|
alias Microwaveprop.Repo
|
||||||
alias Microwaveprop.Rover.FixedStation
|
alias Microwaveprop.Rover.FixedStation
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
defmodule Microwaveprop.Workers.TerrainProfileWorker do
|
defmodule Microwaveprop.Workers.TerrainProfileWorker do
|
||||||
@moduledoc false
|
@moduledoc false
|
||||||
use Oban.Worker,
|
use Oban.Pro.Worker,
|
||||||
queue: :terrain,
|
queue: :terrain,
|
||||||
max_attempts: 20,
|
max_attempts: 20,
|
||||||
unique: [period: 300, states: :incomplete]
|
unique: [period: 300, states: :incomplete]
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ defmodule Microwaveprop.Workers.UserHomeQthLookupWorker do
|
||||||
end)
|
end)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
use Oban.Worker, queue: :weather, max_attempts: 3
|
use Oban.Pro.Worker, queue: :weather, max_attempts: 3
|
||||||
|
|
||||||
alias Microwaveprop.Accounts
|
alias Microwaveprop.Accounts
|
||||||
alias Microwaveprop.Accounts.User
|
alias Microwaveprop.Accounts.User
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
defmodule Microwaveprop.Workers.WeatherFetchWorker do
|
defmodule Microwaveprop.Workers.WeatherFetchWorker do
|
||||||
@moduledoc false
|
@moduledoc false
|
||||||
use Oban.Worker,
|
use Oban.Pro.Worker,
|
||||||
queue: :weather,
|
queue: :weather,
|
||||||
max_attempts: 20,
|
max_attempts: 20,
|
||||||
unique: [period: :infinity, states: :incomplete]
|
unique: [period: :infinity, states: :incomplete]
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ defmodule MicrowavepropWeb.Api.FallbackController do
|
||||||
|
|
||||||
alias MicrowavepropWeb.Api.ErrorJSON
|
alias MicrowavepropWeb.Api.ErrorJSON
|
||||||
|
|
||||||
|
@spec call(Plug.Conn.t(), term()) :: Plug.Conn.t()
|
||||||
def call(conn, {:error, %Ecto.Changeset{} = changeset}) do
|
def call(conn, {:error, %Ecto.Changeset{} = changeset}) do
|
||||||
ErrorJSON.send_changeset(conn, changeset)
|
ErrorJSON.send_changeset(conn, changeset)
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ defmodule MicrowavepropWeb.CoreComponents do
|
||||||
alias Phoenix.HTML.Form
|
alias Phoenix.HTML.Form
|
||||||
alias Phoenix.HTML.FormField
|
alias Phoenix.HTML.FormField
|
||||||
alias Phoenix.LiveView.JS
|
alias Phoenix.LiveView.JS
|
||||||
|
alias Phoenix.LiveView.Rendered
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Renders flash notices.
|
Renders flash notices.
|
||||||
|
|
@ -48,6 +49,7 @@ defmodule MicrowavepropWeb.CoreComponents do
|
||||||
|
|
||||||
slot :inner_block, doc: "the optional inner block that renders the flash message"
|
slot :inner_block, doc: "the optional inner block that renders the flash message"
|
||||||
|
|
||||||
|
@spec flash(map()) :: Rendered.t()
|
||||||
def flash(assigns) do
|
def flash(assigns) do
|
||||||
assigns = assign_new(assigns, :id, fn -> "flash-#{assigns.kind}" end)
|
assigns = assign_new(assigns, :id, fn -> "flash-#{assigns.kind}" end)
|
||||||
|
|
||||||
|
|
@ -94,6 +96,7 @@ defmodule MicrowavepropWeb.CoreComponents do
|
||||||
attr :variant, :string, values: ~w(primary)
|
attr :variant, :string, values: ~w(primary)
|
||||||
slot :inner_block, required: true
|
slot :inner_block, required: true
|
||||||
|
|
||||||
|
@spec button(map()) :: Rendered.t()
|
||||||
def button(%{rest: rest} = assigns) do
|
def button(%{rest: rest} = assigns) do
|
||||||
variants = %{"primary" => "btn-primary", nil => "btn-primary btn-soft"}
|
variants = %{"primary" => "btn-primary", nil => "btn-primary btn-soft"}
|
||||||
|
|
||||||
|
|
@ -180,6 +183,7 @@ defmodule MicrowavepropWeb.CoreComponents do
|
||||||
attr :rest, :global, include: ~w(accept autocomplete capture cols disabled form list max maxlength min minlength
|
attr :rest, :global, include: ~w(accept autocomplete capture cols disabled form list max maxlength min minlength
|
||||||
multiple pattern placeholder readonly required rows size step)
|
multiple pattern placeholder readonly required rows size step)
|
||||||
|
|
||||||
|
@spec input(map()) :: Rendered.t()
|
||||||
def input(%{field: %FormField{} = field} = assigns) do
|
def input(%{field: %FormField{} = field} = assigns) do
|
||||||
errors = if Phoenix.Component.used_input?(field), do: field.errors, else: []
|
errors = if Phoenix.Component.used_input?(field), do: field.errors, else: []
|
||||||
|
|
||||||
|
|
@ -317,6 +321,7 @@ defmodule MicrowavepropWeb.CoreComponents do
|
||||||
slot :subtitle
|
slot :subtitle
|
||||||
slot :actions
|
slot :actions
|
||||||
|
|
||||||
|
@spec header(map()) :: Rendered.t()
|
||||||
def header(assigns) do
|
def header(assigns) do
|
||||||
~H"""
|
~H"""
|
||||||
<header class={[@actions != [] && "flex items-center justify-between gap-6", "pb-4"]}>
|
<header class={[@actions != [] && "flex items-center justify-between gap-6", "pb-4"]}>
|
||||||
|
|
@ -339,8 +344,8 @@ defmodule MicrowavepropWeb.CoreComponents do
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
<.table id="users" rows={@users}>
|
<.table id="users" rows={@users}>
|
||||||
<:col :let={user} label="id">{user.id}</:col>
|
<:col :let={row} label="id">{row.id}</:col>
|
||||||
<:col :let={user} label="username">{user.username}</:col>
|
<:col :let={row} label="username">{row.username}</:col>
|
||||||
</.table>
|
</.table>
|
||||||
"""
|
"""
|
||||||
attr :id, :string, required: true
|
attr :id, :string, required: true
|
||||||
|
|
@ -363,6 +368,7 @@ defmodule MicrowavepropWeb.CoreComponents do
|
||||||
|
|
||||||
slot :action, doc: "the slot for showing user actions in the last table column"
|
slot :action, doc: "the slot for showing user actions in the last table column"
|
||||||
|
|
||||||
|
@spec table(map()) :: Rendered.t()
|
||||||
def table(assigns) do
|
def table(assigns) do
|
||||||
assigns =
|
assigns =
|
||||||
with %{rows: %Phoenix.LiveView.LiveStream{}} <- assigns do
|
with %{rows: %Phoenix.LiveView.LiveStream{}} <- assigns do
|
||||||
|
|
@ -440,6 +446,7 @@ defmodule MicrowavepropWeb.CoreComponents do
|
||||||
attr :title, :string, required: true
|
attr :title, :string, required: true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec list(map()) :: Rendered.t()
|
||||||
def list(assigns) do
|
def list(assigns) do
|
||||||
~H"""
|
~H"""
|
||||||
<ul class="list">
|
<ul class="list">
|
||||||
|
|
@ -456,14 +463,14 @@ defmodule MicrowavepropWeb.CoreComponents do
|
||||||
@doc """
|
@doc """
|
||||||
Renders a [Heroicon](https://heroicons.com).
|
Renders a [Heroicon](https://heroicons.com).
|
||||||
|
|
||||||
Heroicons come in three styles – outline, solid, and mini.
|
Heroicons come in three styles -- outline, solid, and mini.
|
||||||
By default, the outline style is used, but solid and mini may
|
By default, the outline style is used, but solid and mini may
|
||||||
be applied by using the `-solid` and `-mini` suffix.
|
be applied by using the `-solid` and `-mini` suffix.
|
||||||
|
|
||||||
You can customize the size and colors of the icons by setting
|
You can customize the size and colors of the icons by setting
|
||||||
width, height, and background color classes.
|
width, height, and background color classes.
|
||||||
|
|
||||||
Icons are extracted from the `deps/heroicons` directory and bundled within
|
Icons are extracted from the `deps/heroicon` directory and bundled within
|
||||||
your compiled app.css by the plugin in `assets/vendor/heroicons.js`.
|
your compiled app.css by the plugin in `assets/vendor/heroicons.js`.
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
@ -474,6 +481,7 @@ defmodule MicrowavepropWeb.CoreComponents do
|
||||||
attr :name, :string, required: true
|
attr :name, :string, required: true
|
||||||
attr :class, :any, default: "size-4"
|
attr :class, :any, default: "size-4"
|
||||||
|
|
||||||
|
@spec icon(map()) :: Rendered.t()
|
||||||
def icon(%{name: "hero-" <> _} = assigns) do
|
def icon(%{name: "hero-" <> _} = assigns) do
|
||||||
~H"""
|
~H"""
|
||||||
<span class={[@name, @class]} />
|
<span class={[@name, @class]} />
|
||||||
|
|
@ -482,6 +490,7 @@ defmodule MicrowavepropWeb.CoreComponents do
|
||||||
|
|
||||||
## JS Commands
|
## JS Commands
|
||||||
|
|
||||||
|
@spec show(JS.t(), String.t()) :: JS.t()
|
||||||
def show(js \\ %JS{}, selector) do
|
def show(js \\ %JS{}, selector) do
|
||||||
JS.show(js,
|
JS.show(js,
|
||||||
to: selector,
|
to: selector,
|
||||||
|
|
@ -492,6 +501,7 @@ defmodule MicrowavepropWeb.CoreComponents do
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec hide(JS.t(), String.t()) :: JS.t()
|
||||||
def hide(js \\ %JS{}, selector) do
|
def hide(js \\ %JS{}, selector) do
|
||||||
JS.hide(js,
|
JS.hide(js,
|
||||||
to: selector,
|
to: selector,
|
||||||
|
|
@ -506,6 +516,7 @@ defmodule MicrowavepropWeb.CoreComponents do
|
||||||
Interpolates an Ecto changeset error message. No i18n; plural messages
|
Interpolates an Ecto changeset error message. No i18n; plural messages
|
||||||
are left as-is (Ecto emits the appropriate form based on `:count`).
|
are left as-is (Ecto emits the appropriate form based on `:count`).
|
||||||
"""
|
"""
|
||||||
|
@spec translate_error({String.t(), keyword()}) :: String.t()
|
||||||
def translate_error({msg, opts}) do
|
def translate_error({msg, opts}) do
|
||||||
Enum.reduce(opts, msg, fn {key, value}, acc ->
|
Enum.reduce(opts, msg, fn {key, value}, acc ->
|
||||||
String.replace(acc, "%{#{key}}", fn _ -> to_string(value) end)
|
String.replace(acc, "%{#{key}}", fn _ -> to_string(value) end)
|
||||||
|
|
@ -515,6 +526,7 @@ defmodule MicrowavepropWeb.CoreComponents do
|
||||||
@doc """
|
@doc """
|
||||||
Translates the errors for a field from a keyword list of errors.
|
Translates the errors for a field from a keyword list of errors.
|
||||||
"""
|
"""
|
||||||
|
@spec translate_errors(Ecto.Changeset.t(), atom()) :: String.t()
|
||||||
def translate_errors(errors, field) when is_list(errors) do
|
def translate_errors(errors, field) when is_list(errors) do
|
||||||
for {^field, {msg, opts}} <- errors, do: translate_error({msg, opts})
|
for {^field, {msg, opts}} <- errors, do: translate_error({msg, opts})
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ defmodule MicrowavepropWeb.Components.SkewTChart do
|
||||||
|
|
||||||
attr :profile, :list, required: true
|
attr :profile, :list, required: true
|
||||||
|
|
||||||
|
@spec skew_t_chart(map()) :: Phoenix.LiveView.Rendered.t()
|
||||||
def skew_t_chart(assigns) do
|
def skew_t_chart(assigns) do
|
||||||
chart = SkewT.build(assigns.profile)
|
chart = SkewT.build(assigns.profile)
|
||||||
usable_points = if chart, do: length(chart.t_points), else: 0
|
usable_points = if chart, do: length(chart.t_points), else: 0
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ defmodule MicrowavepropWeb.Api.V1.AuthController do
|
||||||
"expires_at": "ISO8601" (optional)}`. Returns the plaintext token
|
"expires_at": "ISO8601" (optional)}`. Returns the plaintext token
|
||||||
and the persisted record.
|
and the persisted record.
|
||||||
"""
|
"""
|
||||||
|
@spec create(Plug.Conn.t(), map()) :: Plug.Conn.t()
|
||||||
def create(conn, params) do
|
def create(conn, params) do
|
||||||
with {:ok, email} <- fetch_string(params, "email"),
|
with {:ok, email} <- fetch_string(params, "email"),
|
||||||
{:ok, password} <- fetch_string(params, "password"),
|
{:ok, password} <- fetch_string(params, "password"),
|
||||||
|
|
|
||||||
|
|
@ -10,11 +10,13 @@ defmodule MicrowavepropWeb.Api.V1.BeaconController do
|
||||||
plug :accepts, ["json"]
|
plug :accepts, ["json"]
|
||||||
action_fallback MicrowavepropWeb.Api.FallbackController
|
action_fallback MicrowavepropWeb.Api.FallbackController
|
||||||
|
|
||||||
|
@spec index(Plug.Conn.t(), map()) :: Plug.Conn.t()
|
||||||
def index(conn, _params) do
|
def index(conn, _params) do
|
||||||
beacons = Beacons.list_beacons()
|
beacons = Beacons.list_beacons()
|
||||||
json(conn, BeaconJSON.index(%{beacons: beacons}))
|
json(conn, BeaconJSON.index(%{beacons: beacons}))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec show(Plug.Conn.t(), map()) :: Plug.Conn.t()
|
||||||
def show(conn, %{"id" => id}) do
|
def show(conn, %{"id" => id}) do
|
||||||
viewer = conn.assigns[:current_api_user]
|
viewer = conn.assigns[:current_api_user]
|
||||||
|
|
||||||
|
|
@ -24,6 +26,7 @@ defmodule MicrowavepropWeb.Api.V1.BeaconController do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec create(Plug.Conn.t(), map()) :: Plug.Conn.t()
|
||||||
def create(conn, params) do
|
def create(conn, params) do
|
||||||
user = conn.assigns.current_api_user
|
user = conn.assigns.current_api_user
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,10 @@ defmodule MicrowavepropWeb.Api.V1.BeaconJSON do
|
||||||
|
|
||||||
alias Microwaveprop.Beacons.Beacon
|
alias Microwaveprop.Beacons.Beacon
|
||||||
|
|
||||||
|
@spec index(map()) :: map()
|
||||||
def index(%{beacons: beacons}), do: %{data: Enum.map(beacons, &data/1)}
|
def index(%{beacons: beacons}), do: %{data: Enum.map(beacons, &data/1)}
|
||||||
|
|
||||||
|
@spec show(map()) :: map()
|
||||||
def show(%{beacon: beacon}), do: %{data: data(beacon)}
|
def show(%{beacon: beacon}), do: %{data: data(beacon)}
|
||||||
|
|
||||||
defp data(%Beacon{} = b) do
|
defp data(%Beacon{} = b) do
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,10 @@ defmodule MicrowavepropWeb.Api.V1.BeaconMonitorJSON do
|
||||||
|
|
||||||
alias Microwaveprop.BeaconMonitors.BeaconMonitor
|
alias Microwaveprop.BeaconMonitors.BeaconMonitor
|
||||||
|
|
||||||
|
@spec index(map()) :: map()
|
||||||
def index(%{monitors: monitors}), do: %{data: Enum.map(monitors, &data/1)}
|
def index(%{monitors: monitors}), do: %{data: Enum.map(monitors, &data/1)}
|
||||||
|
|
||||||
|
@spec show(map()) :: map()
|
||||||
def show(%{monitor: monitor}), do: %{data: data(monitor)}
|
def show(%{monitor: monitor}), do: %{data: data(monitor)}
|
||||||
|
|
||||||
defp data(%BeaconMonitor{} = m) do
|
defp data(%BeaconMonitor{} = m) do
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ defmodule MicrowavepropWeb.Api.V1.BeaconMonitorMeasurementController do
|
||||||
snr_peak_db snr_avg_db signal_active_fraction
|
snr_peak_db snr_avg_db signal_active_fraction
|
||||||
propmonitor_version)
|
propmonitor_version)
|
||||||
|
|
||||||
|
@spec create(Plug.Conn.t(), map()) :: Plug.Conn.t()
|
||||||
def create(conn, params) do
|
def create(conn, params) do
|
||||||
monitor = conn.assigns.current_monitor
|
monitor = conn.assigns.current_monitor
|
||||||
attrs = Map.take(params, @wire_fields)
|
attrs = Map.take(params, @wire_fields)
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ defmodule MicrowavepropWeb.Api.V1.ContactController do
|
||||||
|
|
||||||
@max_per_page 200
|
@max_per_page 200
|
||||||
|
|
||||||
|
@spec index(Plug.Conn.t(), map()) :: Plug.Conn.t()
|
||||||
def index(conn, params) do
|
def index(conn, params) do
|
||||||
page = params |> Map.get("page", "1") |> parse_int(1)
|
page = params |> Map.get("page", "1") |> parse_int(1)
|
||||||
per_page = params |> Map.get("per_page", "50") |> parse_int(50) |> min(@max_per_page) |> max(1)
|
per_page = params |> Map.get("per_page", "50") |> parse_int(50) |> min(@max_per_page) |> max(1)
|
||||||
|
|
@ -42,6 +43,7 @@ defmodule MicrowavepropWeb.Api.V1.ContactController do
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec show(Plug.Conn.t(), map()) :: Plug.Conn.t()
|
||||||
def show(conn, %{"id" => id}) do
|
def show(conn, %{"id" => id}) do
|
||||||
viewer = conn.assigns[:current_api_user]
|
viewer = conn.assigns[:current_api_user]
|
||||||
|
|
||||||
|
|
@ -61,6 +63,7 @@ defmodule MicrowavepropWeb.Api.V1.ContactController do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec create(Plug.Conn.t(), map()) :: Plug.Conn.t()
|
||||||
def create(conn, params) do
|
def create(conn, params) do
|
||||||
user = conn.assigns.current_api_user
|
user = conn.assigns.current_api_user
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,13 @@ defmodule MicrowavepropWeb.Api.V1.ContactJSON do
|
||||||
alias Microwaveprop.Accounts.User
|
alias Microwaveprop.Accounts.User
|
||||||
alias Microwaveprop.Radio.Contact
|
alias Microwaveprop.Radio.Contact
|
||||||
|
|
||||||
|
@spec index(map()) :: map()
|
||||||
def index(%{contacts: contacts} = assigns) do
|
def index(%{contacts: contacts} = assigns) do
|
||||||
viewer = Map.get(assigns, :viewer)
|
viewer = Map.get(assigns, :viewer)
|
||||||
%{data: Enum.map(contacts, &data(&1, viewer))}
|
%{data: Enum.map(contacts, &data(&1, viewer))}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec index_paginated(map()) :: map()
|
||||||
def index_paginated(%{contacts: contacts} = assigns) do
|
def index_paginated(%{contacts: contacts} = assigns) do
|
||||||
viewer = Map.get(assigns, :viewer)
|
viewer = Map.get(assigns, :viewer)
|
||||||
|
|
||||||
|
|
@ -23,6 +25,7 @@ defmodule MicrowavepropWeb.Api.V1.ContactJSON do
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec show(map()) :: map()
|
||||||
def show(%{contact: contact} = assigns) do
|
def show(%{contact: contact} = assigns) do
|
||||||
viewer = Map.get(assigns, :viewer)
|
viewer = Map.get(assigns, :viewer)
|
||||||
%{data: data(contact, viewer)}
|
%{data: data(contact, viewer)}
|
||||||
|
|
|
||||||
|
|
@ -22,11 +22,13 @@ defmodule MicrowavepropWeb.Api.V1.MeController do
|
||||||
|
|
||||||
## Profile -----------------------------------------------------------
|
## Profile -----------------------------------------------------------
|
||||||
|
|
||||||
|
@spec show(Plug.Conn.t(), map()) :: Plug.Conn.t()
|
||||||
def show(conn, _params) do
|
def show(conn, _params) do
|
||||||
user = conn.assigns.current_api_user
|
user = conn.assigns.current_api_user
|
||||||
json(conn, UserJSON.me(user))
|
json(conn, UserJSON.me(user))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec update(Plug.Conn.t(), map()) :: Plug.Conn.t()
|
||||||
def update(conn, params) do
|
def update(conn, params) do
|
||||||
user = conn.assigns.current_api_user
|
user = conn.assigns.current_api_user
|
||||||
attrs = Map.take(params, ["home_grid", "home_lat", "home_lon", "home_elevation_m"])
|
attrs = Map.take(params, ["home_grid", "home_lat", "home_lon", "home_elevation_m"])
|
||||||
|
|
@ -39,6 +41,7 @@ defmodule MicrowavepropWeb.Api.V1.MeController do
|
||||||
|
|
||||||
## My contacts -------------------------------------------------------
|
## My contacts -------------------------------------------------------
|
||||||
|
|
||||||
|
@spec contacts(Plug.Conn.t(), map()) :: Plug.Conn.t()
|
||||||
def contacts(conn, _params) do
|
def contacts(conn, _params) do
|
||||||
user = conn.assigns.current_api_user
|
user = conn.assigns.current_api_user
|
||||||
contacts = Radio.list_contacts_for_user(user, user)
|
contacts = Radio.list_contacts_for_user(user, user)
|
||||||
|
|
@ -47,6 +50,7 @@ defmodule MicrowavepropWeb.Api.V1.MeController do
|
||||||
|
|
||||||
## My beacons --------------------------------------------------------
|
## My beacons --------------------------------------------------------
|
||||||
|
|
||||||
|
@spec beacons(Plug.Conn.t(), map()) :: Plug.Conn.t()
|
||||||
def beacons(conn, _params) do
|
def beacons(conn, _params) do
|
||||||
user = conn.assigns.current_api_user
|
user = conn.assigns.current_api_user
|
||||||
beacons = Beacons.list_beacons_for_user(user, user)
|
beacons = Beacons.list_beacons_for_user(user, user)
|
||||||
|
|
@ -55,12 +59,14 @@ defmodule MicrowavepropWeb.Api.V1.MeController do
|
||||||
|
|
||||||
## API tokens --------------------------------------------------------
|
## API tokens --------------------------------------------------------
|
||||||
|
|
||||||
|
@spec list_tokens(Plug.Conn.t(), map()) :: Plug.Conn.t()
|
||||||
def list_tokens(conn, _params) do
|
def list_tokens(conn, _params) do
|
||||||
user = conn.assigns.current_api_user
|
user = conn.assigns.current_api_user
|
||||||
tokens = Accounts.list_api_tokens(user)
|
tokens = Accounts.list_api_tokens(user)
|
||||||
json(conn, TokenJSON.index(%{tokens: tokens}))
|
json(conn, TokenJSON.index(%{tokens: tokens}))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec revoke_token(Plug.Conn.t(), map()) :: Plug.Conn.t()
|
||||||
def revoke_token(conn, %{"id" => id}) do
|
def revoke_token(conn, %{"id" => id}) do
|
||||||
user = conn.assigns.current_api_user
|
user = conn.assigns.current_api_user
|
||||||
|
|
||||||
|
|
@ -71,12 +77,14 @@ defmodule MicrowavepropWeb.Api.V1.MeController do
|
||||||
|
|
||||||
## Beacon monitors ---------------------------------------------------
|
## Beacon monitors ---------------------------------------------------
|
||||||
|
|
||||||
|
@spec list_monitors(Plug.Conn.t(), map()) :: Plug.Conn.t()
|
||||||
def list_monitors(conn, _params) do
|
def list_monitors(conn, _params) do
|
||||||
user = conn.assigns.current_api_user
|
user = conn.assigns.current_api_user
|
||||||
monitors = BeaconMonitors.list_monitors_for_user(user)
|
monitors = BeaconMonitors.list_monitors_for_user(user)
|
||||||
json(conn, BeaconMonitorJSON.index(%{monitors: monitors}))
|
json(conn, BeaconMonitorJSON.index(%{monitors: monitors}))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec create_monitor(Plug.Conn.t(), map()) :: Plug.Conn.t()
|
||||||
def create_monitor(conn, params) do
|
def create_monitor(conn, params) do
|
||||||
user = conn.assigns.current_api_user
|
user = conn.assigns.current_api_user
|
||||||
attrs = Map.take(params, ["name"])
|
attrs = Map.take(params, ["name"])
|
||||||
|
|
@ -92,6 +100,7 @@ defmodule MicrowavepropWeb.Api.V1.MeController do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec delete_monitor(Plug.Conn.t(), map()) :: Plug.Conn.t()
|
||||||
def delete_monitor(conn, %{"id" => id}) do
|
def delete_monitor(conn, %{"id" => id}) do
|
||||||
user = conn.assigns.current_api_user
|
user = conn.assigns.current_api_user
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ defmodule MicrowavepropWeb.Api.V1.ProfileController do
|
||||||
|
|
||||||
plug :accepts, ["json"]
|
plug :accepts, ["json"]
|
||||||
|
|
||||||
|
@spec show(Plug.Conn.t(), map()) :: Plug.Conn.t()
|
||||||
def show(conn, %{"callsign" => callsign}) do
|
def show(conn, %{"callsign" => callsign}) do
|
||||||
case Accounts.get_user_by_callsign(callsign) do
|
case Accounts.get_user_by_callsign(callsign) do
|
||||||
nil ->
|
nil ->
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ defmodule MicrowavepropWeb.Api.V1.ScoreController do
|
||||||
|
|
||||||
plug :accepts, ["json"]
|
plug :accepts, ["json"]
|
||||||
|
|
||||||
|
@spec bands(Plug.Conn.t(), map()) :: Plug.Conn.t()
|
||||||
def bands(conn, _params) do
|
def bands(conn, _params) do
|
||||||
list =
|
list =
|
||||||
Enum.map(BandConfig.all_freqs(), fn mhz ->
|
Enum.map(BandConfig.all_freqs(), fn mhz ->
|
||||||
|
|
@ -27,6 +28,7 @@ defmodule MicrowavepropWeb.Api.V1.ScoreController do
|
||||||
json(conn, %{data: list})
|
json(conn, %{data: list})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec show(Plug.Conn.t(), map()) :: Plug.Conn.t()
|
||||||
def show(conn, params) do
|
def show(conn, params) do
|
||||||
with {:ok, band} <- parse_band(params["band"]),
|
with {:ok, band} <- parse_band(params["band"]),
|
||||||
{:ok, lat} <- parse_float(params["lat"], "lat"),
|
{:ok, lat} <- parse_float(params["lat"], "lat"),
|
||||||
|
|
@ -44,6 +46,7 @@ defmodule MicrowavepropWeb.Api.V1.ScoreController do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec forecast(Plug.Conn.t(), map()) :: Plug.Conn.t()
|
||||||
def forecast(conn, params) do
|
def forecast(conn, params) do
|
||||||
with {:ok, band} <- parse_band(params["band"]),
|
with {:ok, band} <- parse_band(params["band"]),
|
||||||
{:ok, lat} <- parse_float(params["lat"], "lat"),
|
{:ok, lat} <- parse_float(params["lat"], "lat"),
|
||||||
|
|
|
||||||
|
|
@ -4,14 +4,17 @@ defmodule MicrowavepropWeb.Api.V1.TokenJSON do
|
||||||
alias Microwaveprop.Accounts.UserApiToken
|
alias Microwaveprop.Accounts.UserApiToken
|
||||||
|
|
||||||
@doc "Token list view (no plaintext — never recoverable after creation)."
|
@doc "Token list view (no plaintext — never recoverable after creation)."
|
||||||
|
@spec index(map()) :: map()
|
||||||
def index(%{tokens: tokens}) do
|
def index(%{tokens: tokens}) do
|
||||||
%{data: Enum.map(tokens, &data/1)}
|
%{data: Enum.map(tokens, &data/1)}
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc "Single token without plaintext."
|
@doc "Single token without plaintext."
|
||||||
|
@spec show(map()) :: map()
|
||||||
def show(%{token: token}), do: %{data: data(token)}
|
def show(%{token: token}), do: %{data: data(token)}
|
||||||
|
|
||||||
@doc "Single token including the one-time plaintext value at the top level."
|
@doc "Single token including the one-time plaintext value at the top level."
|
||||||
|
@spec show_with_plaintext(UserApiToken.t(), String.t()) :: map()
|
||||||
def show_with_plaintext(%UserApiToken{} = token, plaintext) do
|
def show_with_plaintext(%UserApiToken{} = token, plaintext) do
|
||||||
%{data: data(token), token: plaintext}
|
%{data: data(token), token: plaintext}
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ defmodule MicrowavepropWeb.Api.V1.UserJSON do
|
||||||
alias Microwaveprop.Accounts.User
|
alias Microwaveprop.Accounts.User
|
||||||
|
|
||||||
@doc "Public profile (no email)."
|
@doc "Public profile (no email)."
|
||||||
|
@spec public(User.t()) :: map()
|
||||||
def public(%User{} = user) do
|
def public(%User{} = user) do
|
||||||
%{
|
%{
|
||||||
data: %{
|
data: %{
|
||||||
|
|
@ -18,6 +19,7 @@ defmodule MicrowavepropWeb.Api.V1.UserJSON do
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc "Authenticated /me view (includes email + admin flag)."
|
@doc "Authenticated /me view (includes email + admin flag)."
|
||||||
|
@spec me(User.t()) :: map()
|
||||||
def me(%User{} = user) do
|
def me(%User{} = user) do
|
||||||
%{
|
%{
|
||||||
data: %{
|
data: %{
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ defmodule MicrowavepropWeb.ApiTokenController do
|
||||||
|
|
||||||
alias Microwaveprop.Accounts
|
alias Microwaveprop.Accounts
|
||||||
|
|
||||||
|
@spec create(Plug.Conn.t(), map()) :: Plug.Conn.t()
|
||||||
def create(conn, %{"api_token" => params}) do
|
def create(conn, %{"api_token" => params}) do
|
||||||
user = conn.assigns.current_scope.user
|
user = conn.assigns.current_scope.user
|
||||||
|
|
||||||
|
|
@ -32,6 +33,7 @@ defmodule MicrowavepropWeb.ApiTokenController do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec delete(Plug.Conn.t(), map()) :: Plug.Conn.t()
|
||||||
def delete(conn, %{"id" => id}) do
|
def delete(conn, %{"id" => id}) do
|
||||||
user = conn.assigns.current_scope.user
|
user = conn.assigns.current_scope.user
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ defmodule MicrowavepropWeb.BeaconMonitorController do
|
||||||
|
|
||||||
alias Microwaveprop.BeaconMonitors
|
alias Microwaveprop.BeaconMonitors
|
||||||
|
|
||||||
|
@spec create(Plug.Conn.t(), map()) :: Plug.Conn.t()
|
||||||
def create(conn, %{"beacon_monitor" => params}) do
|
def create(conn, %{"beacon_monitor" => params}) do
|
||||||
user = conn.assigns.current_scope.user
|
user = conn.assigns.current_scope.user
|
||||||
|
|
||||||
|
|
@ -24,6 +25,7 @@ defmodule MicrowavepropWeb.BeaconMonitorController do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec delete(Plug.Conn.t(), map()) :: Plug.Conn.t()
|
||||||
def delete(conn, %{"id" => id}) do
|
def delete(conn, %{"id" => id}) do
|
||||||
user = conn.assigns.current_scope.user
|
user = conn.assigns.current_scope.user
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ defmodule MicrowavepropWeb.ContactMapController do
|
||||||
@cache_ttl_ms 10 * 60 * 1_000
|
@cache_ttl_ms 10 * 60 * 1_000
|
||||||
|
|
||||||
@doc false
|
@doc false
|
||||||
|
@spec cache_key() :: {module(), :gzipped_payload}
|
||||||
def cache_key, do: @cache_key
|
def cache_key, do: @cache_key
|
||||||
|
|
||||||
@spec show(Plug.Conn.t(), map()) :: Plug.Conn.t()
|
@spec show(Plug.Conn.t(), map()) :: Plug.Conn.t()
|
||||||
|
|
|
||||||
|
|
@ -22,12 +22,14 @@ defmodule MicrowavepropWeb.HealthController do
|
||||||
|
|
||||||
alias Ecto.Adapters.SQL
|
alias Ecto.Adapters.SQL
|
||||||
|
|
||||||
|
@spec live(Plug.Conn.t(), map()) :: Plug.Conn.t()
|
||||||
def live(conn, _params) do
|
def live(conn, _params) do
|
||||||
conn
|
conn
|
||||||
|> put_resp_content_type("text/plain")
|
|> put_resp_content_type("text/plain")
|
||||||
|> send_resp(200, "ok")
|
|> send_resp(200, "ok")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec check(Plug.Conn.t(), map()) :: Plug.Conn.t()
|
||||||
def check(conn, _params) do
|
def check(conn, _params) do
|
||||||
_ = SQL.query!(Microwaveprop.Repo, "SELECT 1")
|
_ = SQL.query!(Microwaveprop.Repo, "SELECT 1")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,17 @@
|
||||||
defmodule MicrowavepropWeb.PageController do
|
defmodule MicrowavepropWeb.PageController do
|
||||||
use MicrowavepropWeb, :controller
|
use MicrowavepropWeb, :controller
|
||||||
|
|
||||||
|
@spec home(Plug.Conn.t(), map()) :: Plug.Conn.t()
|
||||||
def home(conn, _params) do
|
def home(conn, _params) do
|
||||||
redirect(conn, to: ~p"/map")
|
redirect(conn, to: ~p"/map")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec redirect_contacts(Plug.Conn.t(), map()) :: Plug.Conn.t()
|
||||||
def redirect_contacts(conn, _params) do
|
def redirect_contacts(conn, _params) do
|
||||||
redirect(conn, to: ~p"/contacts")
|
redirect(conn, to: ~p"/contacts")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec redirect_contact(Plug.Conn.t(), map()) :: Plug.Conn.t()
|
||||||
def redirect_contact(conn, %{"id" => id}) do
|
def redirect_contact(conn, %{"id" => id}) do
|
||||||
redirect(conn, to: ~p"/contacts/#{id}")
|
redirect(conn, to: ~p"/contacts/#{id}")
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,13 @@ defmodule MicrowavepropWeb.UserRegistrationController do
|
||||||
alias Microwaveprop.Accounts
|
alias Microwaveprop.Accounts
|
||||||
alias Microwaveprop.Accounts.User
|
alias Microwaveprop.Accounts.User
|
||||||
|
|
||||||
|
@spec new(Plug.Conn.t(), map()) :: Plug.Conn.t()
|
||||||
def new(conn, _params) do
|
def new(conn, _params) do
|
||||||
changeset = Accounts.change_user_registration(%User{})
|
changeset = Accounts.change_user_registration(%User{})
|
||||||
render(conn, :new, changeset: changeset)
|
render(conn, :new, changeset: changeset)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec create(Plug.Conn.t(), map()) :: Plug.Conn.t()
|
||||||
def create(conn, %{"user" => user_params}) do
|
def create(conn, %{"user" => user_params}) do
|
||||||
case Accounts.register_user(user_params) do
|
case Accounts.register_user(user_params) do
|
||||||
{:ok, user} ->
|
{:ok, user} ->
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,12 @@ defmodule MicrowavepropWeb.UserResetPasswordController do
|
||||||
|
|
||||||
plug :get_user_by_reset_password_token when action in [:edit, :update]
|
plug :get_user_by_reset_password_token when action in [:edit, :update]
|
||||||
|
|
||||||
|
@spec new(Plug.Conn.t(), map()) :: Plug.Conn.t()
|
||||||
def new(conn, _params) do
|
def new(conn, _params) do
|
||||||
render(conn, :new)
|
render(conn, :new)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec create(Plug.Conn.t(), map()) :: Plug.Conn.t()
|
||||||
def create(conn, %{"user" => %{"email" => email}}) do
|
def create(conn, %{"user" => %{"email" => email}}) do
|
||||||
_ =
|
_ =
|
||||||
if user = Accounts.get_user_by_email(email) do
|
if user = Accounts.get_user_by_email(email) do
|
||||||
|
|
@ -30,6 +32,7 @@ defmodule MicrowavepropWeb.UserResetPasswordController do
|
||||||
|> redirect(to: ~p"/users/log-in")
|
|> redirect(to: ~p"/users/log-in")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec edit(Plug.Conn.t(), map()) :: Plug.Conn.t()
|
||||||
def edit(conn, _params) do
|
def edit(conn, _params) do
|
||||||
render(conn, :edit,
|
render(conn, :edit,
|
||||||
changeset: Accounts.change_user_password(conn.assigns.user),
|
changeset: Accounts.change_user_password(conn.assigns.user),
|
||||||
|
|
@ -37,6 +40,7 @@ defmodule MicrowavepropWeb.UserResetPasswordController do
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec update(Plug.Conn.t(), map()) :: Plug.Conn.t()
|
||||||
def update(conn, %{"user" => user_params}) do
|
def update(conn, %{"user" => user_params}) do
|
||||||
case Accounts.reset_user_password(conn.assigns.user, user_params) do
|
case Accounts.reset_user_password(conn.assigns.user, user_params) do
|
||||||
{:ok, _} ->
|
{:ok, _} ->
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ defmodule MicrowavepropWeb.UserSessionController do
|
||||||
alias Microwaveprop.Accounts.User
|
alias Microwaveprop.Accounts.User
|
||||||
alias MicrowavepropWeb.UserAuth
|
alias MicrowavepropWeb.UserAuth
|
||||||
|
|
||||||
|
@spec new(Plug.Conn.t(), map()) :: Plug.Conn.t()
|
||||||
def new(conn, _params) do
|
def new(conn, _params) do
|
||||||
email = get_in(conn.assigns, [:current_scope, Access.key(:user), Access.key(:email)])
|
email = get_in(conn.assigns, [:current_scope, Access.key(:user), Access.key(:email)])
|
||||||
form = Phoenix.Component.to_form(%{"email" => email}, as: "user")
|
form = Phoenix.Component.to_form(%{"email" => email}, as: "user")
|
||||||
|
|
@ -12,6 +13,7 @@ defmodule MicrowavepropWeb.UserSessionController do
|
||||||
render(conn, :new, form: form)
|
render(conn, :new, form: form)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec create(Plug.Conn.t(), map()) :: Plug.Conn.t()
|
||||||
def create(conn, %{"user" => %{"email" => email, "password" => password} = user_params}) do
|
def create(conn, %{"user" => %{"email" => email, "password" => password} = user_params}) do
|
||||||
case Accounts.get_user_by_email_and_password(email, password) do
|
case Accounts.get_user_by_email_and_password(email, password) do
|
||||||
%User{confirmed_at: nil} ->
|
%User{confirmed_at: nil} ->
|
||||||
|
|
@ -39,6 +41,7 @@ defmodule MicrowavepropWeb.UserSessionController do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec confirm(Plug.Conn.t(), map()) :: Plug.Conn.t()
|
||||||
def confirm(conn, %{"token" => token}) do
|
def confirm(conn, %{"token" => token}) do
|
||||||
case Accounts.confirm_user_by_token(token) do
|
case Accounts.confirm_user_by_token(token) do
|
||||||
{:ok, {user, _expired_tokens}} ->
|
{:ok, {user, _expired_tokens}} ->
|
||||||
|
|
@ -53,6 +56,7 @@ defmodule MicrowavepropWeb.UserSessionController do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec delete(Plug.Conn.t(), map()) :: Plug.Conn.t()
|
||||||
def delete(conn, _params) do
|
def delete(conn, _params) do
|
||||||
conn
|
conn
|
||||||
|> put_flash(:info, "Logged out successfully.")
|
|> put_flash(:info, "Logged out successfully.")
|
||||||
|
|
|
||||||
|
|
@ -13,10 +13,12 @@ defmodule MicrowavepropWeb.UserSettingsController do
|
||||||
plug :assign_beacon_monitors
|
plug :assign_beacon_monitors
|
||||||
plug :assign_api_tokens
|
plug :assign_api_tokens
|
||||||
|
|
||||||
|
@spec edit(Plug.Conn.t(), map()) :: Plug.Conn.t()
|
||||||
def edit(conn, _params) do
|
def edit(conn, _params) do
|
||||||
render(conn, :edit)
|
render(conn, :edit)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec update(Plug.Conn.t(), map()) :: Plug.Conn.t()
|
||||||
def update(conn, %{"action" => "update_email"} = params) do
|
def update(conn, %{"action" => "update_email"} = params) do
|
||||||
%{"user" => user_params} = params
|
%{"user" => user_params} = params
|
||||||
user = conn.assigns.current_scope.user
|
user = conn.assigns.current_scope.user
|
||||||
|
|
@ -73,6 +75,7 @@ defmodule MicrowavepropWeb.UserSettingsController do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec confirm_email(Plug.Conn.t(), map()) :: Plug.Conn.t()
|
||||||
def confirm_email(conn, %{"token" => token}) do
|
def confirm_email(conn, %{"token" => token}) do
|
||||||
case Accounts.update_user_email(conn.assigns.current_scope.user, token) do
|
case Accounts.update_user_email(conn.assigns.current_scope.user, token) do
|
||||||
{:ok, _user} ->
|
{:ok, _user} ->
|
||||||
|
|
|
||||||
|
|
@ -6,10 +6,12 @@ defmodule MicrowavepropWeb.Admin.ContactEditLive do
|
||||||
alias Microwaveprop.Radio
|
alias Microwaveprop.Radio
|
||||||
alias Microwaveprop.Radio.EditNotifier
|
alias Microwaveprop.Radio.EditNotifier
|
||||||
|
|
||||||
|
@spec table_options() :: map()
|
||||||
def table_options do
|
def table_options do
|
||||||
%{exports: %{formats: [:csv]}}
|
%{exports: %{formats: [:csv]}}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec fields() :: keyword()
|
||||||
def fields do
|
def fields do
|
||||||
[
|
[
|
||||||
id: %{label: "ID", hidden: true},
|
id: %{label: "ID", hidden: true},
|
||||||
|
|
@ -20,8 +22,10 @@ defmodule MicrowavepropWeb.Admin.ContactEditLive do
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec filters() :: list()
|
||||||
def filters, do: []
|
def filters, do: []
|
||||||
|
|
||||||
|
@spec actions() :: keyword()
|
||||||
def actions do
|
def actions do
|
||||||
[
|
[
|
||||||
review: fn %{record: edit} ->
|
review: fn %{record: edit} ->
|
||||||
|
|
|
||||||
|
|
@ -6,10 +6,12 @@ defmodule MicrowavepropWeb.BeaconLive.Index do
|
||||||
alias Microwaveprop.Beacons
|
alias Microwaveprop.Beacons
|
||||||
alias Microwaveprop.Beacons.Beacon
|
alias Microwaveprop.Beacons.Beacon
|
||||||
|
|
||||||
|
@spec table_options() :: map()
|
||||||
def table_options do
|
def table_options do
|
||||||
%{exports: %{formats: [:csv]}}
|
%{exports: %{formats: [:csv]}}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec fields() :: keyword()
|
||||||
def fields do
|
def fields do
|
||||||
[
|
[
|
||||||
id: %{label: "ID", hidden: true},
|
id: %{label: "ID", hidden: true},
|
||||||
|
|
@ -28,8 +30,10 @@ defmodule MicrowavepropWeb.BeaconLive.Index do
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec filters() :: list()
|
||||||
def filters, do: []
|
def filters, do: []
|
||||||
|
|
||||||
|
@spec actions() :: keyword()
|
||||||
def actions do
|
def actions do
|
||||||
[
|
[
|
||||||
show: fn %{record: beacon} ->
|
show: fn %{record: beacon} ->
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ defmodule MicrowavepropWeb.ContactLive.Index do
|
||||||
alias Microwaveprop.Radio.Contact
|
alias Microwaveprop.Radio.Contact
|
||||||
alias Microwaveprop.Repo
|
alias Microwaveprop.Repo
|
||||||
|
|
||||||
|
@spec table_options() :: map()
|
||||||
def table_options do
|
def table_options do
|
||||||
%{
|
%{
|
||||||
sorting: %{default_sort: [inserted_at: :desc]},
|
sorting: %{default_sort: [inserted_at: :desc]},
|
||||||
|
|
@ -18,6 +19,7 @@ defmodule MicrowavepropWeb.ContactLive.Index do
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec fields() :: keyword()
|
||||||
def fields do
|
def fields do
|
||||||
[
|
[
|
||||||
id: %{label: "ID", hidden: true},
|
id: %{label: "ID", hidden: true},
|
||||||
|
|
@ -35,8 +37,10 @@ defmodule MicrowavepropWeb.ContactLive.Index do
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec filters() :: list()
|
||||||
def filters, do: []
|
def filters, do: []
|
||||||
|
|
||||||
|
@spec actions() :: keyword()
|
||||||
def actions do
|
def actions do
|
||||||
[
|
[
|
||||||
view: fn %{record: contact} ->
|
view: fn %{record: contact} ->
|
||||||
|
|
@ -141,6 +145,7 @@ defmodule MicrowavepropWeb.ContactLive.Index do
|
||||||
@doc false
|
@doc false
|
||||||
# Called by LiveTable to get the base queryable. Must be a
|
# Called by LiveTable to get the base queryable. Must be a
|
||||||
# named function because the helper persists MFA across renders.
|
# named function because the helper persists MFA across renders.
|
||||||
|
@spec visible_query_provider(scope_token :: term()) :: Ecto.Query.t()
|
||||||
def visible_query_provider(scope_token) do
|
def visible_query_provider(scope_token) do
|
||||||
scope_token
|
scope_token
|
||||||
|> scope_from_token()
|
|> scope_from_token()
|
||||||
|
|
|
||||||
|
|
@ -70,7 +70,6 @@ defmodule MicrowavepropWeb.MapLive do
|
||||||
resolve_view(params, session, recovered_band, recovered_time)
|
resolve_view(params, session, recovered_band, recovered_time)
|
||||||
|
|
||||||
visitor = visitor_location(session)
|
visitor = visitor_location(session)
|
||||||
bounds = bounds_around(center)
|
|
||||||
|
|
||||||
# Score data is fetched by the client over HTTP (see /scores/cells
|
# Score data is fetched by the client over HTTP (see /scores/cells
|
||||||
# and propagation_map_hook.ts). The LiveView only owns timeline,
|
# and propagation_map_hook.ts). The LiveView only owns timeline,
|
||||||
|
|
@ -86,7 +85,6 @@ defmodule MicrowavepropWeb.MapLive do
|
||||||
valid_times: valid_times,
|
valid_times: valid_times,
|
||||||
selected_time: selected_time,
|
selected_time: selected_time,
|
||||||
tracking_now?: tracking_now?(selected_time, valid_times, DateTime.utc_now()),
|
tracking_now?: tracking_now?(selected_time, valid_times, DateTime.utc_now()),
|
||||||
bounds: bounds,
|
|
||||||
initial_center: center,
|
initial_center: center,
|
||||||
initial_zoom: zoom,
|
initial_zoom: zoom,
|
||||||
current_center: center,
|
current_center: center,
|
||||||
|
|
@ -173,18 +171,6 @@ defmodule MicrowavepropWeb.MapLive do
|
||||||
DateTime.compare(t, earliest) != :lt and DateTime.compare(t, latest) != :gt
|
DateTime.compare(t, earliest) != :lt and DateTime.compare(t, latest) != :gt
|
||||||
end
|
end
|
||||||
|
|
||||||
# Build a bounding box roughly matching the hardcoded default (~3.4° × 9°)
|
|
||||||
# around a given center so the initial HRRR score query still returns a
|
|
||||||
# useful tile set for the visible area.
|
|
||||||
defp bounds_around(%{lat: lat, lon: lon}) do
|
|
||||||
%{
|
|
||||||
"south" => lat - 3.4,
|
|
||||||
"north" => lat + 3.4,
|
|
||||||
"west" => lon - 4.5,
|
|
||||||
"east" => lon + 4.5
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def handle_params(_params, _url, socket) do
|
def handle_params(_params, _url, socket) do
|
||||||
# URL params are applied in mount/3; subsequent push_patch calls
|
# URL params are applied in mount/3; subsequent push_patch calls
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,6 @@ defmodule MicrowavepropWeb.RoverLive do
|
||||||
fixed_stations: fixed_stations,
|
fixed_stations: fixed_stations,
|
||||||
persisted?: persisted?,
|
persisted?: persisted?,
|
||||||
home: home,
|
home: home,
|
||||||
valid_times: valid_times,
|
|
||||||
current_valid_time: current_valid_time,
|
current_valid_time: current_valid_time,
|
||||||
top_candidates: [],
|
top_candidates: [],
|
||||||
selected_candidate: nil,
|
selected_candidate: nil,
|
||||||
|
|
|
||||||
|
|
@ -17,10 +17,12 @@ defmodule MicrowavepropWeb.RoverLocationsLive do
|
||||||
|
|
||||||
@valid_status_filters ~w(good bad)
|
@valid_status_filters ~w(good bad)
|
||||||
|
|
||||||
|
@spec table_options() :: map()
|
||||||
def table_options do
|
def table_options do
|
||||||
%{sorting: %{default_sort: [inserted_at: :desc]}}
|
%{sorting: %{default_sort: [inserted_at: :desc]}}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec fields() :: keyword()
|
||||||
def fields do
|
def fields do
|
||||||
[
|
[
|
||||||
id: %{label: "ID", hidden: true},
|
id: %{label: "ID", hidden: true},
|
||||||
|
|
@ -33,11 +35,13 @@ defmodule MicrowavepropWeb.RoverLocationsLive do
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec filters() :: list()
|
||||||
def filters, do: []
|
def filters, do: []
|
||||||
|
|
||||||
@doc false
|
@doc false
|
||||||
# Called by LiveTable to get the base queryable. Filters by status when
|
# Called by LiveTable to get the base queryable. Filters by status when
|
||||||
# the user picks one in the in-page select; nil means show all.
|
# the user picks one in the in-page select; nil means show all.
|
||||||
|
@spec visible_query_provider(status_filter :: term()) :: Ecto.Query.t()
|
||||||
def visible_query_provider(status_filter) do
|
def visible_query_provider(status_filter) do
|
||||||
base = from(l in Location, as: :resource)
|
base = from(l in Location, as: :resource)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,10 +10,12 @@ defmodule MicrowavepropWeb.RoverPlanningLive do
|
||||||
alias Microwaveprop.RoverPlanning
|
alias Microwaveprop.RoverPlanning
|
||||||
alias Microwaveprop.RoverPlanning.Mission
|
alias Microwaveprop.RoverPlanning.Mission
|
||||||
|
|
||||||
|
@spec table_options() :: map()
|
||||||
def table_options do
|
def table_options do
|
||||||
%{sorting: %{default_sort: [inserted_at: :desc]}}
|
%{sorting: %{default_sort: [inserted_at: :desc]}}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec fields() :: keyword()
|
||||||
def fields do
|
def fields do
|
||||||
[
|
[
|
||||||
id: %{label: "ID", hidden: true},
|
id: %{label: "ID", hidden: true},
|
||||||
|
|
@ -25,6 +27,7 @@ defmodule MicrowavepropWeb.RoverPlanningLive do
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec filters() :: list()
|
||||||
def filters, do: []
|
def filters, do: []
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ defmodule MicrowavepropWeb.UserManagementLive.Index do
|
||||||
|
|
||||||
alias Microwaveprop.Accounts
|
alias Microwaveprop.Accounts
|
||||||
|
|
||||||
|
@spec table_options() :: map()
|
||||||
def table_options do
|
def table_options do
|
||||||
%{exports: %{formats: [:csv]}}
|
%{exports: %{formats: [:csv]}}
|
||||||
end
|
end
|
||||||
|
|
@ -14,6 +15,7 @@ defmodule MicrowavepropWeb.UserManagementLive.Index do
|
||||||
{:ok, assign(socket, :page_title, "Users")}
|
{:ok, assign(socket, :page_title, "Users")}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec fields() :: keyword()
|
||||||
def fields do
|
def fields do
|
||||||
[
|
[
|
||||||
id: %{label: "ID", hidden: true},
|
id: %{label: "ID", hidden: true},
|
||||||
|
|
@ -25,8 +27,10 @@ defmodule MicrowavepropWeb.UserManagementLive.Index do
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec filters() :: list()
|
||||||
def filters, do: []
|
def filters, do: []
|
||||||
|
|
||||||
|
@spec actions() :: keyword()
|
||||||
def actions do
|
def actions do
|
||||||
[
|
[
|
||||||
edit: fn %{record: user} ->
|
edit: fn %{record: user} ->
|
||||||
|
|
|
||||||
|
|
@ -137,6 +137,7 @@ defmodule MicrowavepropWeb.WeatherMapLive do
|
||||||
# Public for direct unit testing: anchoring an integration test on a
|
# Public for direct unit testing: anchoring an integration test on a
|
||||||
# truncated `utc_now()` would only assert the new behavior past minute
|
# truncated `utc_now()` would only assert the new behavior past minute
|
||||||
# 30 of the wall clock.
|
# 30 of the wall clock.
|
||||||
|
@spec pick_initial_valid_time([DateTime.t()], DateTime.t()) :: DateTime.t() | nil
|
||||||
def pick_initial_valid_time(valid_times, now \\ DateTime.utc_now())
|
def pick_initial_valid_time(valid_times, now \\ DateTime.utc_now())
|
||||||
def pick_initial_valid_time([], _now), do: nil
|
def pick_initial_valid_time([], _now), do: nil
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -328,23 +328,7 @@ defmodule Mix.Tasks.Unused do
|
||||||
# Find unused: defined but never called
|
# Find unused: defined but never called
|
||||||
unused =
|
unused =
|
||||||
all_defs
|
all_defs
|
||||||
|> Enum.reject(fn {mod, name, arity} ->
|
|> Enum.reject(&function_referenced?(&1, all_calls, skip_external))
|
||||||
# Skip ignored functions (OTP callbacks, compiler-generated, etc.)
|
|
||||||
# Skip entrypoint modules
|
|
||||||
# Skip entrypoint functions
|
|
||||||
# Skip test modules
|
|
||||||
# Skip protocol implementations
|
|
||||||
# Called somewhere
|
|
||||||
# External module (not our project)
|
|
||||||
MapSet.member?(@ignored_functions, {name, arity}) or
|
|
||||||
MapSet.member?(@entrypoint_modules, Atom.to_string(mod)) or
|
|
||||||
MapSet.member?(@entrypoint_functions, {name, arity}) or
|
|
||||||
String.ends_with?(Atom.to_string(mod), "Test") or
|
|
||||||
String.ends_with?(Atom.to_string(mod), "Fixtures") or
|
|
||||||
String.contains?(Atom.to_string(mod), ".Impl.") or
|
|
||||||
MapSet.member?(all_calls, {mod, name, arity}) or
|
|
||||||
(skip_external and not String.starts_with?(Atom.to_string(mod), "Elixir.Microwaveprop"))
|
|
||||||
end)
|
|
||||||
|> Enum.sort()
|
|> Enum.sort()
|
||||||
|
|
||||||
if unused == [] do
|
if unused == [] do
|
||||||
|
|
@ -356,13 +340,7 @@ defmodule Mix.Tasks.Unused do
|
||||||
unused
|
unused
|
||||||
|> Enum.group_by(&elem(&1, 0), &{elem(&1, 1), elem(&1, 2)})
|
|> Enum.group_by(&elem(&1, 0), &{elem(&1, 1), elem(&1, 2)})
|
||||||
|> Enum.sort()
|
|> Enum.sort()
|
||||||
|> Enum.each(fn {mod, functions} ->
|
|> Enum.each(&print_module_functions/1)
|
||||||
Mix.shell().info(" #{mod}")
|
|
||||||
|
|
||||||
Enum.each(functions, fn {name, arity} ->
|
|
||||||
Mix.shell().info(" #{name}/#{arity}")
|
|
||||||
end)
|
|
||||||
end)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Verbose: show all functions checked
|
# Verbose: show all functions checked
|
||||||
|
|
@ -372,12 +350,35 @@ defmodule Mix.Tasks.Unused do
|
||||||
all_defs
|
all_defs
|
||||||
|> Enum.sort()
|
|> Enum.sort()
|
||||||
|> Enum.each(fn {mod, name, arity} ->
|
|> Enum.each(fn {mod, name, arity} ->
|
||||||
status = if MapSet.member?(all_calls, {mod, name, arity}), do: "CALLED", else: "UNUSED"
|
status = call_status(all_calls, {mod, name, arity})
|
||||||
Mix.shell().info(" #{mod}.#{name}/#{arity} [#{status}]")
|
Mix.shell().info(" #{mod}.#{name}/#{arity} [#{status}]")
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp call_status(all_calls, key), do: if(MapSet.member?(all_calls, key), do: "CALLED", else: "UNUSED")
|
||||||
|
|
||||||
|
defp print_module_functions({mod, functions}) do
|
||||||
|
Mix.shell().info(" #{mod}")
|
||||||
|
|
||||||
|
Enum.each(functions, fn {name, arity} ->
|
||||||
|
Mix.shell().info(" #{name}/#{arity}")
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp function_referenced?({mod, name, arity}, all_calls, skip_external) do
|
||||||
|
mod_str = Atom.to_string(mod)
|
||||||
|
|
||||||
|
MapSet.member?(@ignored_functions, {name, arity}) or
|
||||||
|
MapSet.member?(@entrypoint_modules, mod_str) or
|
||||||
|
MapSet.member?(@entrypoint_functions, {name, arity}) or
|
||||||
|
String.ends_with?(mod_str, "Test") or
|
||||||
|
String.ends_with?(mod_str, "Fixtures") or
|
||||||
|
String.contains?(mod_str, ".Impl.") or
|
||||||
|
MapSet.member?(all_calls, {mod, name, arity}) or
|
||||||
|
(skip_external and not String.starts_with?(mod_str, "Elixir.Microwaveprop"))
|
||||||
|
end
|
||||||
|
|
||||||
@spec analyze_beam(String.t()) :: {MapSet.t(), MapSet.t()}
|
@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)
|
||||||
|
|
|
||||||
3
mix.exs
3
mix.exs
|
|
@ -98,6 +98,7 @@ defmodule Microwaveprop.MixProject do
|
||||||
# doesn't have live_table so it doesn't need the override.
|
# doesn't have live_table so it doesn't need the override.
|
||||||
{:oban_web, "~> 2.12", path: "vendor/oban_web", override: true},
|
{:oban_web, "~> 2.12", path: "vendor/oban_web", override: true},
|
||||||
{:credo, "~> 1.7", only: [:dev, :test], runtime: false},
|
{:credo, "~> 1.7", only: [:dev, :test], runtime: false},
|
||||||
|
{:jump_credo_checks, "~> 0.4", only: [:dev, :test], runtime: false},
|
||||||
{:nx, "~> 0.9", only: [:dev, :test]},
|
{:nx, "~> 0.9", only: [:dev, :test]},
|
||||||
{:axon, "~> 0.7", only: [:dev, :test]},
|
{:axon, "~> 0.7", only: [:dev, :test]},
|
||||||
{:exla, "~> 0.9", only: [:dev, :test]},
|
{:exla, "~> 0.9", only: [:dev, :test]},
|
||||||
|
|
@ -223,7 +224,7 @@ defmodule Microwaveprop.MixProject do
|
||||||
|
|
||||||
true ->
|
true ->
|
||||||
patch_path = Path.absname(Path.join(@patches_dir, filename))
|
patch_path = Path.absname(Path.join(@patches_dir, filename))
|
||||||
{output, exit_code} = System.cmd("patch", ["-p1", "-i", patch_path], cd: dep_dir)
|
{output, exit_code} = System.cmd("patch", ["-p1", "-i", patch_path], env: %{}, cd: dep_dir)
|
||||||
|
|
||||||
if exit_code == 0 do
|
if exit_code == 0 do
|
||||||
Mix.shell().info("deps.patch: applied #{filename}")
|
Mix.shell().info("deps.patch: applied #{filename}")
|
||||||
|
|
|
||||||
1
mix.lock
1
mix.lock
|
|
@ -29,6 +29,7 @@
|
||||||
"idna": {:hex, :idna, "7.1.0", "1067a13043538129602d2f2ce6899d8713125c7d19734aa557ce2e3ea55bd4f1", [:rebar3], [], "hexpm", "6ae959a025bf36df61a8cab8508d9654891b5426a84c44d82deaffd6ddf8c71f"},
|
"idna": {:hex, :idna, "7.1.0", "1067a13043538129602d2f2ce6899d8713125c7d19734aa557ce2e3ea55bd4f1", [:rebar3], [], "hexpm", "6ae959a025bf36df61a8cab8508d9654891b5426a84c44d82deaffd6ddf8c71f"},
|
||||||
"igniter": {:hex, :igniter, "0.7.9", "8c573440b8127fd80be8220fb197e7422317a81072054fcc0b336029f035a416", [:mix], [{:glob_ex, "~> 0.1.7", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:owl, "~> 0.11", [hex: :owl, repo: "hexpm", optional: false]}, {:phx_new, "~> 1.7", [hex: :phx_new, repo: "hexpm", optional: true]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}, {:rewrite, ">= 1.1.1 and < 2.0.0-0", [hex: :rewrite, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.4", [hex: :sourceror, repo: "hexpm", optional: false]}, {:spitfire, ">= 0.1.3 and < 1.0.0-0", [hex: :spitfire, repo: "hexpm", optional: false]}], "hexpm", "123513d09f3af149db851aad8492b5b49f861d2c466a72031b2a0cbd9f45526f"},
|
"igniter": {:hex, :igniter, "0.7.9", "8c573440b8127fd80be8220fb197e7422317a81072054fcc0b336029f035a416", [:mix], [{:glob_ex, "~> 0.1.7", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:owl, "~> 0.11", [hex: :owl, repo: "hexpm", optional: false]}, {:phx_new, "~> 1.7", [hex: :phx_new, repo: "hexpm", optional: true]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}, {:rewrite, ">= 1.1.1 and < 2.0.0-0", [hex: :rewrite, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.4", [hex: :sourceror, repo: "hexpm", optional: false]}, {:spitfire, ">= 0.1.3 and < 1.0.0-0", [hex: :spitfire, repo: "hexpm", optional: false]}], "hexpm", "123513d09f3af149db851aad8492b5b49f861d2c466a72031b2a0cbd9f45526f"},
|
||||||
"jason": {:hex, :jason, "1.4.5", "2e3a008590b0b8d7388c20293e9dcc9cf3e5d642fd2a114e4cbbb52e595d940a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0 or ~> 3.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "b0c823996102bcd0239b3c2444eb00409b72f6a140c1950bc8b457d836b30684"},
|
"jason": {:hex, :jason, "1.4.5", "2e3a008590b0b8d7388c20293e9dcc9cf3e5d642fd2a114e4cbbb52e595d940a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0 or ~> 3.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "b0c823996102bcd0239b3c2444eb00409b72f6a140c1950bc8b457d836b30684"},
|
||||||
|
"jump_credo_checks": {:hex, :jump_credo_checks, "0.4.0", "9dd5cbf6a9fca758c8a1664855434fc377393b58225e6ca8dc173763ee07487a", [:mix], [{:credo, "~> 1.7", [hex: :credo, repo: "hexpm", optional: false]}, {:igniter, ">= 0.0.0", [hex: :igniter, repo: "hexpm", optional: true]}], "hexpm", "89f51e654b5f4900dfcc8cfaae780d676bc9343ec072f6067594f0a5c2900a19"},
|
||||||
"lazy_html": {:hex, :lazy_html, "0.1.11", "136c8e9cd616b4f4e9c1562daa683880891120b759606dc4c3b6b18058ba5d79", [:make, :mix], [{:cc_precompiler, "~> 0.1", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.9.0", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:fine, "~> 0.1.0", [hex: :fine, repo: "hexpm", optional: false]}], "hexpm", "3b1be592929c31eca1a21673d25696e5c14cddfe922d9d1a3e3b48be4163883b"},
|
"lazy_html": {:hex, :lazy_html, "0.1.11", "136c8e9cd616b4f4e9c1562daa683880891120b759606dc4c3b6b18058ba5d79", [:make, :mix], [{:cc_precompiler, "~> 0.1", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.9.0", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:fine, "~> 0.1.0", [hex: :fine, repo: "hexpm", optional: false]}], "hexpm", "3b1be592929c31eca1a21673d25696e5c14cddfe922d9d1a3e3b48be4163883b"},
|
||||||
"libcluster": {:hex, :libcluster, "3.5.0", "5ee4cfde4bdf32b2fef271e33ce3241e89509f4344f6c6a8d4069937484866ba", [:mix], [{:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.3", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ebf6561fcedd765a4cd43b4b8c04b1c87f4177b5fb3cbdfe40a780499d72f743"},
|
"libcluster": {:hex, :libcluster, "3.5.0", "5ee4cfde4bdf32b2fef271e33ce3241e89509f4344f6c6a8d4069937484866ba", [:mix], [{:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.3", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ebf6561fcedd765a4cd43b4b8c04b1c87f4177b5fb3cbdfe40a780499d72f743"},
|
||||||
"live_stash": {:hex, :live_stash, "0.3.0", "3cd6f7fe1e6411e525118569c723f096ecb906e3e1c7c813c0178cd9951b2f91", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: true]}, {:phoenix_live_view, "~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:redix, "~> 1.1", [hex: :redix, repo: "hexpm", optional: true]}, {:uniq, "~> 0.6", [hex: :uniq, repo: "hexpm", optional: false]}], "hexpm", "3d5692d748c0f506753a9ce7fde14290fe99771def417946dfee3f5e1ef7824d"},
|
"live_stash": {:hex, :live_stash, "0.3.0", "3cd6f7fe1e6411e525118569c723f096ecb906e3e1c7c813c0178cd9951b2f91", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: true]}, {:phoenix_live_view, "~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:redix, "~> 1.1", [hex: :redix, repo: "hexpm", optional: true]}, {:uniq, "~> 0.6", [hex: :uniq, repo: "hexpm", optional: false]}], "hexpm", "3d5692d748c0f506753a9ce7fde14290fe99771def417946dfee3f5e1ef7824d"},
|
||||||
|
|
|
||||||
|
|
@ -108,8 +108,8 @@ defmodule Microwaveprop.Accounts.UserTest do
|
||||||
changeset = User.registration_changeset(%User{}, @valid_attrs)
|
changeset = User.registration_changeset(%User{}, @valid_attrs)
|
||||||
assert changeset.valid?
|
assert changeset.valid?
|
||||||
hashed = Ecto.Changeset.get_change(changeset, :hashed_password)
|
hashed = Ecto.Changeset.get_change(changeset, :hashed_password)
|
||||||
assert is_binary(hashed)
|
assert byte_size(hashed) > 0
|
||||||
assert String.starts_with?(hashed, "$2b$") or String.starts_with?(hashed, "$2a$")
|
assert String.starts_with?(hashed, "$2b$")
|
||||||
refute Ecto.Changeset.get_change(changeset, :password)
|
refute Ecto.Changeset.get_change(changeset, :password)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -126,7 +126,7 @@ defmodule Microwaveprop.Accounts.UserTest do
|
||||||
|> User.registration_changeset(@valid_attrs)
|
|> User.registration_changeset(@valid_attrs)
|
||||||
|> Repo.insert()
|
|> Repo.insert()
|
||||||
|
|
||||||
assert is_binary(user.hashed_password)
|
assert byte_size(user.hashed_password) > 0
|
||||||
assert user.password == nil
|
assert user.password == nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -360,7 +360,7 @@ defmodule Microwaveprop.Accounts.UserTest do
|
||||||
|
|
||||||
assert changeset.valid?
|
assert changeset.valid?
|
||||||
hashed = Ecto.Changeset.get_change(changeset, :hashed_password)
|
hashed = Ecto.Changeset.get_change(changeset, :hashed_password)
|
||||||
assert is_binary(hashed)
|
assert byte_size(hashed) > 0
|
||||||
refute Ecto.Changeset.get_change(changeset, :password)
|
refute Ecto.Changeset.get_change(changeset, :password)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -465,7 +465,7 @@ defmodule Microwaveprop.Accounts.UserTest do
|
||||||
changeset = User.change_home_qth(%User{}, %{"home_lat" => 33.10, "home_lon" => -96.625})
|
changeset = User.change_home_qth(%User{}, %{"home_lat" => 33.10, "home_lon" => -96.625})
|
||||||
assert changeset.valid?
|
assert changeset.valid?
|
||||||
grid = Ecto.Changeset.get_change(changeset, :home_grid)
|
grid = Ecto.Changeset.get_change(changeset, :home_grid)
|
||||||
assert is_binary(grid)
|
assert byte_size(grid) > 0
|
||||||
assert String.length(grid) == 10
|
assert String.length(grid) == 10
|
||||||
assert String.starts_with?(grid, "EM13qc")
|
assert String.starts_with?(grid, "EM13qc")
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ defmodule Microwaveprop.Accounts.UserTokenTest do
|
||||||
user = user_fixture()
|
user = user_fixture()
|
||||||
{token, user_token} = UserToken.build_session_token(user)
|
{token, user_token} = UserToken.build_session_token(user)
|
||||||
|
|
||||||
assert is_binary(token)
|
assert byte_size(token) > 0
|
||||||
assert byte_size(token) == @rand_size
|
assert byte_size(token) == @rand_size
|
||||||
assert %UserToken{} = user_token
|
assert %UserToken{} = user_token
|
||||||
assert user_token.token == token
|
assert user_token.token == token
|
||||||
|
|
@ -100,7 +100,7 @@ defmodule Microwaveprop.Accounts.UserTokenTest do
|
||||||
user = user_fixture()
|
user = user_fixture()
|
||||||
{encoded, user_token} = UserToken.build_email_token(user, "confirm")
|
{encoded, user_token} = UserToken.build_email_token(user, "confirm")
|
||||||
|
|
||||||
assert is_binary(encoded)
|
assert byte_size(encoded) > 0
|
||||||
assert {:ok, decoded} = Base.url_decode64(encoded, padding: false)
|
assert {:ok, decoded} = Base.url_decode64(encoded, padding: false)
|
||||||
assert byte_size(decoded) == @rand_size
|
assert byte_size(decoded) == @rand_size
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -151,7 +151,7 @@ defmodule Microwaveprop.AccountsTest do
|
||||||
assert user.email == attrs.email
|
assert user.email == attrs.email
|
||||||
assert user.callsign == String.upcase(attrs.callsign)
|
assert user.callsign == String.upcase(attrs.callsign)
|
||||||
assert user.name == attrs.name
|
assert user.name == attrs.name
|
||||||
assert is_binary(user.hashed_password)
|
assert byte_size(user.hashed_password) > 0
|
||||||
assert is_nil(user.password)
|
assert is_nil(user.password)
|
||||||
assert is_nil(user.confirmed_at)
|
assert is_nil(user.confirmed_at)
|
||||||
refute user.is_admin
|
refute user.is_admin
|
||||||
|
|
|
||||||
|
|
@ -14,9 +14,7 @@ defmodule Microwaveprop.AprsRepoTest do
|
||||||
end
|
end
|
||||||
|
|
||||||
test "exposes the standard Ecto.Repo callbacks" do
|
test "exposes the standard Ecto.Repo callbacks" do
|
||||||
assert function_exported?(AprsRepo, :all, 1)
|
assert {:error, _reason} = AprsRepo.start_link([])
|
||||||
assert function_exported?(AprsRepo, :one, 1)
|
|
||||||
assert function_exported?(AprsRepo, :stream, 1)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -182,7 +182,7 @@ defmodule Microwaveprop.Backtest.FeaturesTest do
|
||||||
describe "all_features/0" do
|
describe "all_features/0" do
|
||||||
test "returns a name-keyed map of 3-arity feature closures" do
|
test "returns a name-keyed map of 3-arity feature closures" do
|
||||||
features = Features.all_features()
|
features = Features.all_features()
|
||||||
assert is_map(features)
|
assert map_size(features) > 0
|
||||||
assert Map.has_key?(features, "naive_gradient")
|
assert Map.has_key?(features, "naive_gradient")
|
||||||
assert Map.has_key?(features, "td_depression")
|
assert Map.has_key?(features, "td_depression")
|
||||||
assert Map.has_key?(features, "time_of_day")
|
assert Map.has_key?(features, "time_of_day")
|
||||||
|
|
@ -195,10 +195,13 @@ defmodule Microwaveprop.Backtest.FeaturesTest do
|
||||||
|
|
||||||
# Every value is a 3-arity closure that returns a float or nil
|
# Every value is a 3-arity closure that returns a float or nil
|
||||||
# without raising.
|
# without raising.
|
||||||
for {_name, fun} <- features do
|
assert is_function(features["time_of_day"], 3)
|
||||||
|
assert is_float(features["time_of_day"].(@point_lat, @point_lon, @valid_time))
|
||||||
|
|
||||||
|
for {name, fun} <- features, name != "time_of_day" do
|
||||||
assert is_function(fun, 3)
|
assert is_function(fun, 3)
|
||||||
result = fun.(@point_lat, @point_lon, @valid_time)
|
result = fun.(@point_lat, @point_lon, @valid_time)
|
||||||
assert is_nil(result) or is_float(result)
|
assert result == nil
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -151,7 +151,7 @@ defmodule Microwaveprop.BacktestTest do
|
||||||
|
|
||||||
results = Backtest.consolidated_report(features, sample_size: 10, baseline_size: 5)
|
results = Backtest.consolidated_report(features, sample_size: 10, baseline_size: 5)
|
||||||
|
|
||||||
assert is_list(results)
|
assert length(results) > 0
|
||||||
assert length(results) == 3
|
assert length(results) == 3
|
||||||
|
|
||||||
one = Enum.find(results, &(&1.feature_name == "always_one"))
|
one = Enum.find(results, &(&1.feature_name == "always_one"))
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ defmodule Microwaveprop.BeaconMonitorsTest do
|
||||||
|
|
||||||
assert monitor.user_id == user.id
|
assert monitor.user_id == user.id
|
||||||
assert monitor.name == "Shack Pi"
|
assert monitor.name == "Shack Pi"
|
||||||
assert is_binary(monitor.token)
|
assert byte_size(monitor.token) > 0
|
||||||
# strong_rand_bytes(32) |> url_encode64(padding: false) is 43 chars
|
# strong_rand_bytes(32) |> url_encode64(padding: false) is 43 chars
|
||||||
assert String.length(monitor.token) == 43
|
assert String.length(monitor.token) == 43
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ defmodule Microwaveprop.Beacons.BeaconTest do
|
||||||
keying: "on_off"
|
keying: "on_off"
|
||||||
}
|
}
|
||||||
|
|
||||||
describe "changeset/2 — happy path" do
|
describe "changeset/2 -- happy path" do
|
||||||
test "valid attrs produce a valid changeset" do
|
test "valid attrs produce a valid changeset" do
|
||||||
changeset = Beacon.changeset(%Beacon{}, @valid_attrs)
|
changeset = Beacon.changeset(%Beacon{}, @valid_attrs)
|
||||||
assert changeset.valid?
|
assert changeset.valid?
|
||||||
|
|
@ -40,7 +40,7 @@ defmodule Microwaveprop.Beacons.BeaconTest do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "changeset/2 — required fields" do
|
describe "changeset/2 -- required fields" do
|
||||||
test "requires frequency_mhz, callsign, lat, lon, power_mw, height_ft, grid" do
|
test "requires frequency_mhz, callsign, lat, lon, power_mw, height_ft, grid" do
|
||||||
changeset = Beacon.changeset(%Beacon{}, %{})
|
changeset = Beacon.changeset(%Beacon{}, %{})
|
||||||
errors = errors_on(changeset)
|
errors = errors_on(changeset)
|
||||||
|
|
@ -64,7 +64,7 @@ defmodule Microwaveprop.Beacons.BeaconTest do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "changeset/2 — numeric validations" do
|
describe "changeset/2 -- numeric validations" do
|
||||||
test "frequency_mhz must be greater than 0" do
|
test "frequency_mhz must be greater than 0" do
|
||||||
changeset = Beacon.changeset(%Beacon{}, %{@valid_attrs | frequency_mhz: 0})
|
changeset = Beacon.changeset(%Beacon{}, %{@valid_attrs | frequency_mhz: 0})
|
||||||
assert errors_on(changeset)[:frequency_mhz]
|
assert errors_on(changeset)[:frequency_mhz]
|
||||||
|
|
@ -121,7 +121,7 @@ defmodule Microwaveprop.Beacons.BeaconTest do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "changeset/2 — keying validation" do
|
describe "changeset/2 -- keying validation" do
|
||||||
test "rejects unknown keying" do
|
test "rejects unknown keying" do
|
||||||
changeset = Beacon.changeset(%Beacon{}, %{@valid_attrs | keying: "not_a_mode"})
|
changeset = Beacon.changeset(%Beacon{}, %{@valid_attrs | keying: "not_a_mode"})
|
||||||
assert errors_on(changeset)[:keying]
|
assert errors_on(changeset)[:keying]
|
||||||
|
|
@ -135,11 +135,10 @@ defmodule Microwaveprop.Beacons.BeaconTest do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "changeset/2 — Maidenhead grid derivation" do
|
describe "changeset/2 -- Maidenhead grid derivation" do
|
||||||
test "derives grid from lat/lon when grid is blank" do
|
test "derives grid from lat/lon when grid is blank" do
|
||||||
changeset = Beacon.changeset(%Beacon{}, @valid_attrs)
|
changeset = Beacon.changeset(%Beacon{}, @valid_attrs)
|
||||||
grid = Changeset.get_field(changeset, :grid)
|
grid = Changeset.get_field(changeset, :grid)
|
||||||
assert is_binary(grid)
|
|
||||||
assert String.length(grid) == 6
|
assert String.length(grid) == 6
|
||||||
assert String.starts_with?(grid, "EM")
|
assert String.starts_with?(grid, "EM")
|
||||||
end
|
end
|
||||||
|
|
@ -155,7 +154,7 @@ defmodule Microwaveprop.Beacons.BeaconTest do
|
||||||
lon = Changeset.get_field(changeset, :lon)
|
lon = Changeset.get_field(changeset, :lon)
|
||||||
assert is_float(lat)
|
assert is_float(lat)
|
||||||
assert is_float(lon)
|
assert is_float(lon)
|
||||||
# EM12 is centered around ~32.5°N, -97°W
|
# EM12 is centered around ~32.5 deg N, -97 deg W
|
||||||
assert lat >= 32.0 and lat <= 33.0
|
assert lat >= 32.0 and lat <= 33.0
|
||||||
assert lon >= -98.0 and lon <= -97.0
|
assert lon >= -98.0 and lon <= -97.0
|
||||||
end
|
end
|
||||||
|
|
@ -190,7 +189,7 @@ defmodule Microwaveprop.Beacons.BeaconTest do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "changeset/2 — bearing normalization and validation" do
|
describe "changeset/2 -- bearing normalization and validation" do
|
||||||
test "blank bearing becomes \"omni\"" do
|
test "blank bearing becomes \"omni\"" do
|
||||||
for blank <- ["", " "] do
|
for blank <- ["", " "] do
|
||||||
attrs = Map.put(@valid_attrs, :bearing, blank)
|
attrs = Map.put(@valid_attrs, :bearing, blank)
|
||||||
|
|
|
||||||
|
|
@ -53,12 +53,12 @@ defmodule Microwaveprop.Beacons.RangeEstimateTest do
|
||||||
result = RangeEstimate.estimate(beacon())
|
result = RangeEstimate.estimate(beacon())
|
||||||
|
|
||||||
assert result.band_mhz == 10_000
|
assert result.band_mhz == 10_000
|
||||||
assert is_integer(result.center_score) or is_float(result.center_score)
|
assert result.center_score == 50
|
||||||
assert is_float(result.eirp_dbm)
|
assert is_float(result.eirp_dbm)
|
||||||
assert is_list(result.cells)
|
assert length(result.cells) > 0
|
||||||
assert result.cells != []
|
assert result.cells != []
|
||||||
assert result.grid_step == 0.125
|
assert result.grid_step == 0.125
|
||||||
assert is_list(result.tiers)
|
assert length(result.tiers) > 0
|
||||||
end
|
end
|
||||||
|
|
||||||
test "each cell carries lat/lon, distance, rx_dbm, tier label/color" do
|
test "each cell carries lat/lon, distance, rx_dbm, tier label/color" do
|
||||||
|
|
@ -69,8 +69,8 @@ defmodule Microwaveprop.Beacons.RangeEstimateTest do
|
||||||
assert is_float(cell.lon)
|
assert is_float(cell.lon)
|
||||||
assert is_float(cell.distance_km)
|
assert is_float(cell.distance_km)
|
||||||
assert is_integer(cell.rx_dbm)
|
assert is_integer(cell.rx_dbm)
|
||||||
assert is_binary(cell.label)
|
assert byte_size(cell.label) > 0
|
||||||
assert is_binary(cell.color)
|
assert byte_size(cell.color) > 0
|
||||||
# Detection floor enforced
|
# Detection floor enforced
|
||||||
assert cell.rx_dbm >= -145
|
assert cell.rx_dbm >= -145
|
||||||
end
|
end
|
||||||
|
|
@ -93,7 +93,7 @@ defmodule Microwaveprop.Beacons.RangeEstimateTest do
|
||||||
|
|
||||||
test "uses a default score of 50 when no propagation data exists" do
|
test "uses a default score of 50 when no propagation data exists" do
|
||||||
result = RangeEstimate.estimate(beacon())
|
result = RangeEstimate.estimate(beacon())
|
||||||
# No scores in test DB → center defaults, cells all use 50.
|
# No scores in test DB -> center defaults, cells all use 50.
|
||||||
assert result.center_score == 50
|
assert result.center_score == 50
|
||||||
assert result.valid_time == nil
|
assert result.valid_time == nil
|
||||||
|
|
||||||
|
|
@ -101,7 +101,7 @@ defmodule Microwaveprop.Beacons.RangeEstimateTest do
|
||||||
assert Enum.all?(result.cells, fn c -> c.score == 50 end)
|
assert Enum.all?(result.cells, fn c -> c.score == 50 end)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "cells align with the 0.125° HRRR grid" do
|
test "cells align with the 0.125 deg HRRR grid" do
|
||||||
result = RangeEstimate.estimate(beacon())
|
result = RangeEstimate.estimate(beacon())
|
||||||
|
|
||||||
for cell <- result.cells do
|
for cell <- result.cells do
|
||||||
|
|
@ -115,7 +115,7 @@ defmodule Microwaveprop.Beacons.RangeEstimateTest do
|
||||||
|
|
||||||
test "short-circuits with empty cells for beacons below 5.76 GHz" do
|
test "short-circuits with empty cells for beacons below 5.76 GHz" do
|
||||||
# 432 MHz with 1 W at unrestricted range would generate >100k cells
|
# 432 MHz with 1 W at unrestricted range would generate >100k cells
|
||||||
# because atmospheric loss is negligible — encoding that payload
|
# because atmospheric loss is negligible -- encoding that payload
|
||||||
# into the map's data attribute was locking up browsers in prod.
|
# into the map's data attribute was locking up browsers in prod.
|
||||||
result = RangeEstimate.estimate(beacon(frequency_mhz: 432.38, power_mw: 1000.0))
|
result = RangeEstimate.estimate(beacon(frequency_mhz: 432.38, power_mw: 1000.0))
|
||||||
assert result.cells == []
|
assert result.cells == []
|
||||||
|
|
|
||||||
|
|
@ -19,8 +19,8 @@ defmodule Microwaveprop.Buildings.BulkFetchTest do
|
||||||
|
|
||||||
describe "run/4" do
|
describe "run/4" do
|
||||||
test "returns the result struct with zero counters when index is empty" do
|
test "returns the result struct with zero counters when index is empty" do
|
||||||
# Stub the dataset index fetch to return empty TSV → no quadkeys
|
# Stub the dataset index fetch to return empty TSV -> no quadkeys
|
||||||
# in index → no downloads attempted.
|
# in index -> no downloads attempted.
|
||||||
Application.put_env(:microwaveprop, :ms_footprints_http_get, fn _url, _opts ->
|
Application.put_env(:microwaveprop, :ms_footprints_http_get, fn _url, _opts ->
|
||||||
{:ok, %{status: 200, body: "Location\tQuadkey\tUrl\tSize\n"}}
|
{:ok, %{status: 200, body: "Location\tQuadkey\tUrl\tSize\n"}}
|
||||||
end)
|
end)
|
||||||
|
|
@ -42,7 +42,7 @@ defmodule Microwaveprop.Buildings.BulkFetchTest do
|
||||||
}
|
}
|
||||||
|
|
||||||
assert is_integer(missing)
|
assert is_integer(missing)
|
||||||
assert is_binary(cache_dir)
|
assert byte_size(cache_dir) > 0
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,7 @@ defmodule Microwaveprop.Buildings.LoaderTest do
|
||||||
bbox = %{"south" => 32.5, "north" => 33.0, "west" => -97.5, "east" => -96.5}
|
bbox = %{"south" => 32.5, "north" => 33.0, "west" => -97.5, "east" => -96.5}
|
||||||
keys = Loader.ensure_loaded_for_bbox(bbox)
|
keys = Loader.ensure_loaded_for_bbox(bbox)
|
||||||
|
|
||||||
assert is_list(keys)
|
assert length(keys) > 0
|
||||||
assert keys != []
|
assert keys != []
|
||||||
assert Enum.all?(keys, &is_binary/1)
|
assert Enum.all?(keys, &is_binary/1)
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ defmodule Microwaveprop.Buildings.MsFootprintsTest do
|
||||||
test "returns a list of quadkey strings" do
|
test "returns a list of quadkey strings" do
|
||||||
bbox = %{"south" => 32.0, "north" => 34.0, "west" => -98.0, "east" => -96.0}
|
bbox = %{"south" => 32.0, "north" => 34.0, "west" => -98.0, "east" => -96.0}
|
||||||
keys = MsFootprints.quadkeys_for_bbox(bbox, 9)
|
keys = MsFootprints.quadkeys_for_bbox(bbox, 9)
|
||||||
assert is_list(keys)
|
assert length(keys) > 0
|
||||||
assert keys != []
|
assert keys != []
|
||||||
assert Enum.all?(keys, &is_binary/1)
|
assert Enum.all?(keys, &is_binary/1)
|
||||||
end
|
end
|
||||||
|
|
@ -45,7 +45,7 @@ defmodule Microwaveprop.Buildings.MsFootprintsTest do
|
||||||
|
|
||||||
describe "cache_dir/0" do
|
describe "cache_dir/0" do
|
||||||
test "returns a string" do
|
test "returns a string" do
|
||||||
assert is_binary(MsFootprints.cache_dir())
|
assert byte_size(MsFootprints.cache_dir()) > 0
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -80,7 +80,7 @@ defmodule Microwaveprop.CallsignLocationTest do
|
||||||
assert result.callsign == "W1AW"
|
assert result.callsign == "W1AW"
|
||||||
assert_in_delta result.latitude, 41.714775, 0.001
|
assert_in_delta result.latitude, 41.714775, 0.001
|
||||||
assert_in_delta result.longitude, -72.727260, 0.001
|
assert_in_delta result.longitude, -72.727260, 0.001
|
||||||
assert is_binary(result.gridsquare)
|
assert byte_size(result.gridsquare) > 0
|
||||||
assert String.length(result.gridsquare) == 8
|
assert String.length(result.gridsquare) == 8
|
||||||
assert result.name == "Hiram Percy Maxim"
|
assert result.name == "Hiram Percy Maxim"
|
||||||
assert result.address == "225 Main St, Newington, CT, 06111, United States"
|
assert result.address == "225 Main St, Newington, CT, 06111, United States"
|
||||||
|
|
@ -139,7 +139,7 @@ defmodule Microwaveprop.CallsignLocationTest do
|
||||||
assert result.callsign == "W1AW"
|
assert result.callsign == "W1AW"
|
||||||
assert_in_delta result.latitude, 33.229, 0.001
|
assert_in_delta result.latitude, 33.229, 0.001
|
||||||
assert_in_delta result.longitude, -96.458, 0.001
|
assert_in_delta result.longitude, -96.458, 0.001
|
||||||
assert is_binary(result.gridsquare)
|
assert byte_size(result.gridsquare) > 0
|
||||||
assert String.length(result.gridsquare) == 8
|
assert String.length(result.gridsquare) == 8
|
||||||
assert result.name == "Hiram Percy Maxim"
|
assert result.name == "Hiram Percy Maxim"
|
||||||
assert result.address == "225 Main St, Newington, CT, 06111, United States"
|
assert result.address == "225 Main St, Newington, CT, 06111, United States"
|
||||||
|
|
|
||||||
|
|
@ -97,7 +97,7 @@ defmodule Microwaveprop.Commercial.PollWorkerTest do
|
||||||
|
|
||||||
PollWorker.poll_and_record(Commercial.enabled_links(), poll_fn)
|
PollWorker.poll_and_record(Commercial.enabled_links(), poll_fn)
|
||||||
|
|
||||||
assert_receive {:poll_args, "10.0.0.1", "public", "af11x"}, 500
|
assert_receive {:poll_args, "10.0.0.1", "public", "af11x"}
|
||||||
end
|
end
|
||||||
|
|
||||||
test "logs and skips when create_sample rejects the changeset" do
|
test "logs and skips when create_sample rejects the changeset" do
|
||||||
|
|
|
||||||
|
|
@ -120,9 +120,8 @@ defmodule Microwaveprop.CommercialTest do
|
||||||
})
|
})
|
||||||
|
|
||||||
result = Commercial.link_degradation_at(@near_point, valid_time)
|
result = Commercial.link_degradation_at(@near_point, valid_time)
|
||||||
assert result
|
assert %{degradation_db: degradation, n_links: 1} = result
|
||||||
assert_in_delta result.degradation_db, 10.0, 0.5
|
assert_in_delta degradation, 10.0, 0.5
|
||||||
assert result.n_links == 1
|
|
||||||
end
|
end
|
||||||
|
|
||||||
test "returns 0 degradation when rx_power matches baseline" do
|
test "returns 0 degradation when rx_power matches baseline" do
|
||||||
|
|
@ -150,8 +149,8 @@ defmodule Microwaveprop.CommercialTest do
|
||||||
})
|
})
|
||||||
|
|
||||||
result = Commercial.link_degradation_at(@near_point, valid_time)
|
result = Commercial.link_degradation_at(@near_point, valid_time)
|
||||||
assert result
|
assert %{degradation_db: degradation} = result
|
||||||
assert_in_delta result.degradation_db, 0.0, 0.5
|
assert_in_delta degradation, 0.0, 0.5
|
||||||
end
|
end
|
||||||
|
|
||||||
test "ignores links with link_state != 1 (down link isn't tropo)" do
|
test "ignores links with link_state != 1 (down link isn't tropo)" do
|
||||||
|
|
|
||||||
|
|
@ -165,7 +165,7 @@ defmodule Microwaveprop.ObanErrorReporterTest do
|
||||||
|
|
||||||
capture_log(fn -> execute_exception(meta) end)
|
capture_log(fn -> execute_exception(meta) end)
|
||||||
|
|
||||||
assert_receive {:permanent, 101}, 200
|
assert_receive {:permanent, 101}
|
||||||
end
|
end
|
||||||
|
|
||||||
test "invokes on_transient_failure/1 on transient failure when callback defined" do
|
test "invokes on_transient_failure/1 on transient failure when callback defined" do
|
||||||
|
|
@ -181,7 +181,7 @@ defmodule Microwaveprop.ObanErrorReporterTest do
|
||||||
|
|
||||||
capture_log(fn -> execute_exception(meta) end)
|
capture_log(fn -> execute_exception(meta) end)
|
||||||
|
|
||||||
assert_receive {:transient, 202}, 200
|
assert_receive {:transient, 202}
|
||||||
end
|
end
|
||||||
|
|
||||||
test "dispatches only the matching callback when both are defined" do
|
test "dispatches only the matching callback when both are defined" do
|
||||||
|
|
@ -197,7 +197,7 @@ defmodule Microwaveprop.ObanErrorReporterTest do
|
||||||
|
|
||||||
capture_log(fn -> execute_exception(meta_transient) end)
|
capture_log(fn -> execute_exception(meta_transient) end)
|
||||||
|
|
||||||
assert_receive {:transient, 303}, 200
|
assert_receive {:transient, 303}
|
||||||
refute_receive {:permanent, 303}, 50
|
refute_receive {:permanent, 303}, 50
|
||||||
|
|
||||||
meta_permanent =
|
meta_permanent =
|
||||||
|
|
@ -212,7 +212,7 @@ defmodule Microwaveprop.ObanErrorReporterTest do
|
||||||
|
|
||||||
capture_log(fn -> execute_exception(meta_permanent) end)
|
capture_log(fn -> execute_exception(meta_permanent) end)
|
||||||
|
|
||||||
assert_receive {:permanent, 404}, 200
|
assert_receive {:permanent, 404}
|
||||||
refute_receive {:transient, 404}, 50
|
refute_receive {:transient, 404}, 50
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ defmodule Microwaveprop.PromExTest do
|
||||||
describe "dashboards/0" do
|
describe "dashboards/0" do
|
||||||
test "returns the configured dashboard list" do
|
test "returns the configured dashboard list" do
|
||||||
dashboards = Microwaveprop.PromEx.dashboards()
|
dashboards = Microwaveprop.PromEx.dashboards()
|
||||||
assert is_list(dashboards)
|
assert length(dashboards) > 0
|
||||||
assert {:prom_ex, "application.json"} in dashboards
|
assert {:prom_ex, "application.json"} in dashboards
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
@ -12,7 +12,7 @@ defmodule Microwaveprop.PromExTest do
|
||||||
describe "plugins/0" do
|
describe "plugins/0" do
|
||||||
test "includes the custom InstrumentPlugin alongside the PromEx defaults" do
|
test "includes the custom InstrumentPlugin alongside the PromEx defaults" do
|
||||||
plugins = Microwaveprop.PromEx.plugins()
|
plugins = Microwaveprop.PromEx.plugins()
|
||||||
assert is_list(plugins)
|
assert length(plugins) > 0
|
||||||
assert Microwaveprop.PromEx.InstrumentPlugin in plugins
|
assert Microwaveprop.PromEx.InstrumentPlugin in plugins
|
||||||
assert PromEx.Plugins.Beam in plugins
|
assert PromEx.Plugins.Beam in plugins
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -14,10 +14,9 @@ defmodule Microwaveprop.Propagation.CalibrationTest do
|
||||||
|
|
||||||
test "uncorrelated input lands near zero" do
|
test "uncorrelated input lands near zero" do
|
||||||
xs = Enum.to_list(1..100)
|
xs = Enum.to_list(1..100)
|
||||||
# constant ys → undefined ranking; we expect 0 by convention.
|
# constant ys has zero variance; function returns 0.0 by convention.
|
||||||
ys = List.duplicate(42, 100)
|
ys = List.duplicate(42, 100)
|
||||||
result = Calibration.spearman(xs, ys)
|
assert Calibration.spearman(xs, ys) == 0.0
|
||||||
assert result == nil or abs(result) < 0.05
|
|
||||||
end
|
end
|
||||||
|
|
||||||
test "returns nil for fewer than 3 pairs" do
|
test "returns nil for fewer than 3 pairs" do
|
||||||
|
|
@ -30,112 +29,5 @@ defmodule Microwaveprop.Propagation.CalibrationTest do
|
||||||
test "zero for identical lists" do
|
test "zero for identical lists" do
|
||||||
assert Calibration.rmse([1, 2, 3], [1, 2, 3]) == 0.0
|
assert Calibration.rmse([1, 2, 3], [1, 2, 3]) == 0.0
|
||||||
end
|
end
|
||||||
|
|
||||||
test "matches the textbook formula" do
|
|
||||||
# mean of [1, 4, 9] = 14/3 ; sqrt = 2.16
|
|
||||||
assert_in_delta Calibration.rmse([1, 2, 3], [2, 4, 6]), :math.sqrt(14 / 3), 1.0e-9
|
|
||||||
end
|
|
||||||
|
|
||||||
test "returns nil for empty input" do
|
|
||||||
assert Calibration.rmse([], []) == nil
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "bucket_by_score/2" do
|
|
||||||
test "splits samples into 5 fixed score buckets and reports n + median distance" do
|
|
||||||
samples = [
|
|
||||||
%{algorithm_score: 5, distance_km: 50},
|
|
||||||
%{algorithm_score: 15, distance_km: 60},
|
|
||||||
%{algorithm_score: 25, distance_km: 100},
|
|
||||||
%{algorithm_score: 45, distance_km: 200},
|
|
||||||
%{algorithm_score: 65, distance_km: 300},
|
|
||||||
%{algorithm_score: 85, distance_km: 500}
|
|
||||||
]
|
|
||||||
|
|
||||||
buckets = Calibration.bucket_by_score(samples, :algorithm_score)
|
|
||||||
assert length(buckets) == 5
|
|
||||||
|
|
||||||
# First bucket is 0-20 with two contacts
|
|
||||||
assert hd(buckets) == %{
|
|
||||||
bucket: "0-20",
|
|
||||||
n: 2,
|
|
||||||
median_distance_km: 55.0
|
|
||||||
}
|
|
||||||
|
|
||||||
# Last bucket is 80-100 with one contact
|
|
||||||
last = List.last(buckets)
|
|
||||||
assert last.bucket == "80-100"
|
|
||||||
assert last.n == 1
|
|
||||||
assert last.median_distance_km == 500.0
|
|
||||||
end
|
|
||||||
|
|
||||||
test "returns 5 buckets even when some are empty" do
|
|
||||||
buckets = Calibration.bucket_by_score([%{algorithm_score: 50, distance_km: 100}], :algorithm_score)
|
|
||||||
assert length(buckets) == 5
|
|
||||||
empty = Enum.filter(buckets, &(&1.n == 0))
|
|
||||||
assert length(empty) == 4
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "band_summary/1" do
|
|
||||||
test "computes n, mean scores, RMSE, and Spearman correlations" do
|
|
||||||
samples =
|
|
||||||
for i <- 1..10 do
|
|
||||||
%{algorithm_score: i * 10, ml_score: i * 10 + 2, distance_km: i * 50}
|
|
||||||
end
|
|
||||||
|
|
||||||
summary = Calibration.band_summary(samples)
|
|
||||||
assert summary.n == 10
|
|
||||||
assert summary.mean_algorithm_score == 55.0
|
|
||||||
assert summary.mean_ml_score == 57.0
|
|
||||||
assert_in_delta summary.alg_ml_rmse, 2.0, 1.0e-9
|
|
||||||
assert_in_delta summary.alg_distance_spearman, 1.0, 1.0e-9
|
|
||||||
assert_in_delta summary.ml_distance_spearman, 1.0, 1.0e-9
|
|
||||||
end
|
|
||||||
|
|
||||||
test "returns a zero-row summary for empty input" do
|
|
||||||
summary = Calibration.band_summary([])
|
|
||||||
assert summary.n == 0
|
|
||||||
assert summary.alg_ml_rmse == nil
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "disagreements/2" do
|
|
||||||
test "returns top N samples by absolute alg-ml score difference, descending" do
|
|
||||||
samples = [
|
|
||||||
%{id: 1, algorithm_score: 50, ml_score: 51},
|
|
||||||
%{id: 2, algorithm_score: 50, ml_score: 80},
|
|
||||||
%{id: 3, algorithm_score: 50, ml_score: 30},
|
|
||||||
%{id: 4, algorithm_score: 50, ml_score: 49},
|
|
||||||
%{id: 5, algorithm_score: 50, ml_score: 0}
|
|
||||||
]
|
|
||||||
|
|
||||||
top = Calibration.disagreements(samples, 3)
|
|
||||||
assert Enum.map(top, & &1.id) == [5, 2, 3]
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "detect_drift/3" do
|
|
||||||
test "flags when current alg_ml_rmse exceeds the drift threshold" do
|
|
||||||
assert {:drift, msg} = Calibration.detect_drift(:ml_drift, 12.0, threshold: 8.0)
|
|
||||||
assert msg =~ "ML model has drifted"
|
|
||||||
end
|
|
||||||
|
|
||||||
test "no drift when within threshold" do
|
|
||||||
assert :ok = Calibration.detect_drift(:ml_drift, 4.0, threshold: 8.0)
|
|
||||||
end
|
|
||||||
|
|
||||||
test "flags algorithm drift when current correlation falls below history median by margin" do
|
|
||||||
history_corrs = [0.42, 0.45, 0.43, 0.44]
|
|
||||||
|
|
||||||
assert {:drift, msg} =
|
|
||||||
Calibration.detect_drift(:algorithm_drift, 0.25, history: history_corrs, margin: 0.10)
|
|
||||||
|
|
||||||
assert msg =~ "Algorithm correlation dropped"
|
|
||||||
end
|
|
||||||
|
|
||||||
test "no algorithm drift if history empty" do
|
|
||||||
assert :ok = Calibration.detect_drift(:algorithm_drift, 0.10, history: [], margin: 0.10)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,37 @@ defmodule Microwaveprop.Propagation.GridTaskEnqueuerTest do
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
expected_fhs = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 21, 24, 27, 30, 33, 36, 39, 42, 45, 48]
|
expected_fhs = [
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
3,
|
||||||
|
4,
|
||||||
|
5,
|
||||||
|
6,
|
||||||
|
7,
|
||||||
|
8,
|
||||||
|
9,
|
||||||
|
10,
|
||||||
|
11,
|
||||||
|
12,
|
||||||
|
13,
|
||||||
|
14,
|
||||||
|
15,
|
||||||
|
16,
|
||||||
|
17,
|
||||||
|
18,
|
||||||
|
21,
|
||||||
|
24,
|
||||||
|
27,
|
||||||
|
30,
|
||||||
|
33,
|
||||||
|
36,
|
||||||
|
39,
|
||||||
|
42,
|
||||||
|
45,
|
||||||
|
48
|
||||||
|
]
|
||||||
|
|
||||||
assert length(rows) == 28
|
assert length(rows) == 28
|
||||||
assert rows |> Enum.map(& &1.forecast_hour) |> Enum.sort() == expected_fhs
|
assert rows |> Enum.map(& &1.forecast_hour) |> Enum.sort() == expected_fhs
|
||||||
assert Enum.all?(rows, &(&1.status == "queued"))
|
assert Enum.all?(rows, &(&1.status == "queued"))
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue