prop/lib/microwaveprop/release.ex
Graham McInitre b65d3227cd feat: clickable band filters on PSK Reporter tab, fix all test & credo issues
PSK Reporter tab:
- Fix handle_info(:refresh_spots) to use start_async instead of blocking
- Add require Logger to fix compiler warning
- Make band counts clickable: clicking a band filters table to that band's
  last 100 spots; clicking again clears the filter
- Highlight active filter badge, show Clear filter link when filtered
- Update subtitle and empty state dynamically based on filter
- Filter persists across auto-refresh (60s)
- Fix empty state condition to render immediately without async guard

Credo fixes (mix credo --strict now passes):
- weather_map_component.ex: replace @doc false with @impl true (missing spec)
- contact_weather_enqueue_worker.ex: extract hrrr_placeholder_for_contact/3
  to reduce nesting depth
- profile_lookup.ex: replace MapSet+then+reject pattern with Enum.filter
  to fix both nesting depth and cyclomatic complexity

Test infrastructure fixes:
- Fix Release.migrate/0 to handle repos without migration directories
  (AprsRepo in test has no migrations)
- Fix insert_spot helper: add missing 19th param for inserted_at/updated_at
- Fix String.index/2 -> :binary.match for Elixir 1.20 compat
- Fix DateTime.add!/3 -> DateTime.add/3 for Elixir 1.20 compat
- Fix substring matching in limit test (GRID1 matched GRID10)
- Add Process.sleep+render calls for async band_counts in new tests
- Replace impossible 'empty state for filtered band' test with
  'switching band filter updates table' test
- All 19 tests pass, mix credo --strict clean
2026-07-20 13:01:00 -05:00

135 lines
4.4 KiB
Elixir

defmodule Microwaveprop.Release do
@moduledoc """
Release tasks that run in production without Mix installed.
Long-running tasks are enqueued as Oban jobs so `eval` returns
immediately. Check Oban Web at /admin/oban for progress.
Usage via `bin/microwaveprop eval`:
bin/microwaveprop eval "Microwaveprop.Release.migrate"
bin/microwaveprop eval "Microwaveprop.Release.backtest_all"
bin/microwaveprop eval "Microwaveprop.Release.backtest(\"naive_gradient\")"
bin/microwaveprop eval "Microwaveprop.Release.climatology"
bin/microwaveprop eval "Microwaveprop.Release.native_backfill(500)"
bin/microwaveprop eval "Microwaveprop.Release.native_derive"
bin/microwaveprop eval "Microwaveprop.Release.recalibrate"
"""
alias Microwaveprop.Workers.AdminTaskWorker
alias Microwaveprop.Workers.HrrrNativeGridWorker
@app :microwaveprop
@spec migrate() :: [:ok | {:error, term()}]
def migrate do
_ = load_app()
for repo <- repos() do
case Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :up, all: true)) do
{:ok, _, _} -> :ok
error -> error
end
end
end
@spec rollback(module(), integer()) :: {:ok, term(), term()}
def rollback(repo, version) do
_ = load_app()
{:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :down, to: version))
end
@spec backtest(String.t()) :: :ok
def backtest(feature_name) when is_binary(feature_name) do
_ = start_app()
{:ok, job} = Oban.insert(AdminTaskWorker.new(%{task: "backtest", feature: feature_name}))
IO.puts("Enqueued backtest for #{feature_name} (job #{job.id})")
end
@spec backtest_all() :: :ok
def backtest_all do
_ = start_app()
{:ok, job} = Oban.insert(AdminTaskWorker.new(%{task: "backtest_all"}))
IO.puts("Enqueued consolidated backtest (job #{job.id})")
end
@spec climatology(pos_integer()) :: :ok
def climatology(min_samples \\ 3) do
_ = start_app()
{:ok, job} = Oban.insert(AdminTaskWorker.new(%{task: "climatology", min_samples: min_samples}))
IO.puts("Enqueued climatology build (job #{job.id})")
end
@spec native_backfill(pos_integer()) :: :ok
def native_backfill(limit \\ 500) do
_ = start_app()
%{rows: rows} =
Microwaveprop.Repo.query!(
"""
SELECT EXTRACT(YEAR FROM qso_timestamp)::int AS year,
EXTRACT(MONTH FROM qso_timestamp)::int AS month,
EXTRACT(DAY FROM qso_timestamp)::int AS day,
EXTRACT(HOUR FROM qso_timestamp)::int AS hour,
COUNT(*) AS cnt
FROM contacts
WHERE pos1 IS NOT NULL
AND qso_timestamp >= '2019-01-01'
GROUP BY 1, 2, 3, 4
ORDER BY cnt DESC
LIMIT $1
""",
[limit],
timeout: 60_000
)
IO.puts("Enqueuing #{length(rows)} native backfill jobs...")
for [year, month, day, hour, cnt] <- rows do
args = %{year: year, month: month, day: day, hour: hour}
case Oban.insert(HrrrNativeGridWorker.new(args, unique: [period: :infinity])) do
{:ok, _} -> IO.puts(" #{year}-#{month}-#{day} #{hour}Z (#{cnt} contacts)")
{:error, _} -> IO.puts(" #{year}-#{month}-#{day} #{hour}Z (skipped)")
end
end
IO.puts("Done.")
end
@spec recalibrate() :: :ok
def recalibrate do
_ = start_app()
{:ok, job} = Oban.insert(AdminTaskWorker.new(%{task: "recalibrate"}))
IO.puts("Enqueued recalibration (job #{job.id})")
end
# scorer_diff removed — the tool depended on per-cell factors in
# propagation_scores, which are gone now that scores live as binary
# files on disk. Left as a stub that tells the operator what
# happened if they shell into a running release from muscle memory.
@spec scorer_diff(String.t()) :: :ok
def scorer_diff(_new_weights_json) do
_ = start_app()
IO.puts("scorer_diff is disabled: propagation_scores table + factors storage have been dropped.")
end
@spec native_derive(pos_integer()) :: :ok
def native_derive(limit \\ 10_000) do
_ = start_app()
{:ok, job} = Oban.insert(AdminTaskWorker.new(%{task: "native_derive", limit: limit}))
IO.puts("Enqueued native derive (job #{job.id})")
end
defp repos do
Application.fetch_env!(@app, :ecto_repos)
end
defp load_app do
{:ok, _} = Application.ensure_all_started(:ssl)
Application.ensure_loaded(@app)
end
defp start_app do
Application.ensure_all_started(@app)
end
end