prop-ci:latest on the registry may not have the latest Dockerfile.ci
changes when the test step runs. Build the image locally using
--cache-from for speed, then use the local tag.
Logger.put_module_level suppressed the log output that ObanErrorReporterTest
asserts on via capture_log. The worker-level suppression (NarrFetchWorker)
already handles the noise source.
EXLA's cached_make copies libexla.so but Make still compiles .o files
when they're missing from deps/exla/cache/ (they don't survive
mix deps.get). Save the pre-built cache/ directory at /opt/exla-cache/
in the CI image and restore it after mix deps.get in the test step.
With both .o files and libexla.so present, Make skips all C++ compilation.
EXLA's cached_make needs Mix.Tasks.Compile.ElixirMake (from elixir_make),
which must be compiled first. mix deps.compile exla alone doesn't handle
transitive compile-time deps. Use mix deps.compile without args to
compile in dependency order.
- Dockerfile.ci: compile xla dep before exla — exla's extract_xla
calls XLA.archive_path!/0 which requires xla to be compiled first
- Fold build-ci-image into build.yaml as prerequisite job so tests
always have the precompiled EXLA image available
- Remove standalone build-ci-image.yaml (redundant)
Add Dockerfile.ci that extends the hexpm/elixir image with precompiled
EXLA NIF cached at ~/.cache/xla/. When mix test runs, EXLA's cached_make
finds the precompiled libexla.so and skips the OOM-prone g++ C++
compilation entirely.
- Dockerfile.ci: two-stage build — builder compiles EXLA into cache;
final stage copies the cache and installs test deps (cdo)
- .forgejo/workflows/build-ci-image.yaml: builds and pushes prop-ci
image on mix.exs/mix.lock/vendor changes + weekly cron
- .forgejo/workflows/build.yaml: test step uses prop-ci:latest instead
of raw hexpm/elixir image, removing apt-get + hex/rebar install steps
RoverPathProfileWorker broadcasts {:rover_path_updated, path_id} after
updating a path. handle_info fetches via get_path!/1 and splices into
@paths, but get_path!/1 did not preload :rover_location — causing a
KeyError on %Ecto.Association.NotLoaded{} when the template accessed
location.id. Preload [:rover_location, :station, :mission] matching
the pattern in list_paths/1 and path_live.ex.
The ANALYZE queries run via Task.Supervised which doesn't hold
sandbox ownership. rescue only catches exceptions; sandbox-pool
checkout failures come as exit signals. Add catch :exit so the
Task logs a warning instead of crashing.
- nix/shell.nix: remove export MIX_ENV=dev — it overrode Mix's automatic
env selection, causing mix test to load dev config with no sandbox pool
- config/runtime.exs: use Ecto.Adapters.SQL.Sandbox pool (not
DBConnection.Ownership) for test env — test_helper.exs uses the
Sandbox API which requires the sandbox pool type
- test/test_helper.exs: silence NarrFetchWorker and ObanErrorReporter
log noise at :critical level — expected error-path output when inline
Oban jobs hit stubbed HTTP 500s
- conn_case: remove compile-time alias for AccountsFixtures to fix compilation ordering warning
- hrdps_client_test: clear global hrdps_cycle_available_fn in test setup so injected http_head takes effect (2 previously failing tests)
- test_helper: use Code.ensure_loaded!/1 instead of Code.require_file to avoid redefining module warnings
- mix.exs: add test_ignore_filters to suppress Elixir 1.20 test support file warnings
HrdpsClient already calls Req.get/head with req_options() that
merge :hrdps_req_options from config. Add plug: {Req.Test, HrdpsClient}
to test config so Req.Test.stub intercepts HTTP calls — same pattern
as HrrrClient. Also add hrdps_cycle_available_fn default so the
HTTP probe doesn't fire in tests.
Replace the env-based Application.put_env stubs in DataCase with
Req.Test.stub(HrdpsClient, 404).
Fire-and-forget tasks spawned via async_stream_nolink on the
PartitionSupervisor outlive the test sandbox owner in :manual mode.
When the owner exits, descendant tasks get shutdown on next DB
checkout — the process crash is logged at [error] by Task.Supervised.
Same benign sandbox-cleanup category as the already-suppressed
Postgrex.Protocol, DBConnection.Connection, and DBConnection.Holder.
Add default NarrClient 404 stub to DataCase so incidental inline
NarrFetchWorker execution doesn't log 500 errors in unrelated tests.
Add DBConnection.Holder to the sandbox-cleanup noise suppression list
— same benign 'owner exited' category as the already-suppressed
Postgrex.Protocol and DBConnection.Connection.
Replace the non-idiomatic :auto sandbox mode with :manual, eliminating
the root cause of stale data across test runs. The :auto mode let any
process auto-checkout and commit outside test transactions — Oban in
testing:inline already disables plugins/queues, so :auto was never
needed.
Changes:
- test_helper: Sandbox.mode(Repo, :manual); drop manual app boot (mix
now starts the app idiomatically), drop --no-start alias, drop
table-cleanup loop
- DataCase: single reset_test_state/0 consolidates GridCache.clear,
ScoreCache.clear, score-file wipe, and default HTTP stubs; drop
the :auto-restore on_exit hack; setup_sandbox now just start_owner
- ConnCase: delegates shared reset to DataCase, removes duplicate
cache clears and stub installs
- config: pool Ecto.Adapters.SQL.Sandbox (idiomatic canonical form)
Note: GridCache/ScoreCache are globally-registered GenServers, so
the architecturally clean per-test start_supervised! approach
conflicts with async:true (parallel tests can't register the same
name). The centralized clear in reset_test_state is the practical
compromise — one place, documented, single call.
patch_beacons_json/2 and remove_from_beacons_json/2 were calling
Enum.reject/2 directly on @beacons_json, which is a JSON-encoded
string from Jason.encode!/0 (not a list). This crashed with
Protocol.UndefinedError when no approved beacons existed (string
was "[]") and a PubSub broadcast triggered an update.
Decode with Jason.decode!/1, operate on the list, then re-encode
with Jason.encode!/1 so the template's data-beacons attribute
remains a valid JSON string for the JS hook.
Rows left behind by a crashed/killed prior run persist because
sandbox on_exit callbacks don't fire on BEAM crash or SIGKILL.
Wipe known test-data tables (contacts, hrrr_fetch_tasks,
iemre_observations, oban_jobs) after app boot so every suite
starts clean regardless of previous run state.
Create shared ContactsFixtures module with globally-unique qso_timestamp
seconds to prevent unique-constraint violations when async test modules
run in parallel and insert contacts with identical dedup-key columns.
The qso_timestamp column is timestamp(0), so microsecond offsets were
truncated — use System.unique_integer monotonic seconds instead.
Also make count-asserting tests resilient to sandbox-leaked contacts
from prior tests by using >= assertions or status-based checks rather
than exact counts.
Includes automated DateTime.add → DateTime.shift migration from
mix format.
The Elixir 1.20 code path workaround adds stream_data's ebin but
doesn't start the application. stream_data's macro calls
Application.fetch_env!(:stream_data, :initial_size), which requires
the application to be loaded/started.
198+ concurrent async tests overwhelm the default 50ms queue_target
with ownership pool checkouts, causing frequent :queue_timeout errors.
The 500ms/5000ms values are standard for high-concurrency test suites.
- wgrib2_test: accept both :wgrib2_not_available (no wgrib2) and binary
error message (wgrib2 available via nix) for nonexistent-file tests.
- narr_client_test + narr_fetch_worker_test: add cdo_has_proj? guard that
skips cdo-dependent tests when cdo lacks proj support (e.g. nixpkgs cdo).
Use System.find_executable to avoid ErlangError when cdo not on PATH.
All 4054 tests pass, 6 skipped (fixture/cdo guards).
- nix/wgrib2.nix: build wgrib2 3.8.0 from source with NCEPLIBS-g2c 2.3.0,
matching the Dockerfile.base build.
- flake.nix: wire wgrib2 derivation into dev shell so direnv loads it.
- DataCase: call Req.Test.set_req_test_from_context/1 in setup so
async:false tests get shared Req.Test mode, making stubs visible
to Tasks spawned by async_stream_nolink.
- hrrr_client_test: async:true => async:false so Req.Test stubs are
visible in Task.Supervisor.async_stream_nolink worker processes.
- weather_test: add setup blocks to latest_grid_valid_time/0 and
available_weather_valid_times/0 describe blocks that delete
is_grid_point=true HrrrProfile rows before each test, preventing
cross-contamination from other async test modules.
- contact_map_controller_test: call RateLimiter.reset() in setup so
accumulated ETS counters from prior tests don't trigger 429s.
- Add Phoenix.Ecto.SQL.Sandbox.allow to all start_async closures in ContactLive.Show (8 sites) and PskrSpotsLive (4 sites), fixing silent empty results when async Tasks accessed DB in tests
- Add File.exists? skip-guards to wgrib2_test.exs (14 guards) and hrrr_client_test.exs (3 guards) so tests skip gracefully when GRIB2 fixtures are absent
- Remove leftover debug IO.puts from SandboxHook
- Fix stale homepage assertion in html_module_test.exs
- Add cdo and wgrib2 to nix shell.nix and Forgejo CI apt-get install
Test suite: 4049/4054 passed (99.9%), 5 remaining failures are environment gaps (cdo/wgrib2 not yet in CI runtime)
- Delete Microwaveprop.Aprs, AprsRepo, Aprs.PathParser modules
- Delete Mix.Tasks.Calibrate.Aprs144 mix task
- Remove AprsRepo from application.ex children and config files
- Remove APRS test files and sandbox references in test_helper
- Fix credo nested-module alias suggestions in conn_case.ex
- get_user_by_callsign tests: use unique callsign per test to avoid sandbox collision
- Admin email test: merge into single changeset-level test to avoid shared-sandbox FK constraint violations on rover_locations
- Remove 4 dead {1, nil} = return-value assertions from Repo.update_all calls
- Use DBConnection.Ownership pool (not Ecto.Adapters.SQL.Sandbox) to
satisfy Ecto 3.14's Sandbox.start_owner!/2 pool check
- Set pool in config/test.exs and config/runtime.exs before app start
- mix test --no-start via alias prevents Mix from launching app before
test_helper can configure the sandbox pool
- ExUnit.start() without capture_log: true avoids Logger cascade
- :auto sandbox mode lets background processes (migrations, backfill,
Oban) access the DB during Application boot
- Load test support modules and dep ebin paths in test_helper for
Elixir 1.20 lazy test compilation
- Switch polylines and circle markers to shared L.canvas() renderer,
eliminating ~58k SVG DOM nodes in favor of 1 GPU-composited canvas
- Add server-side callsign/date filtering to /api/contacts/map endpoint
to reduce payload size when filters are active
- Add early-exit fast path in applyCallsignFilter when no filter active
- Batch rebuildDots with requestAnimationFrame to avoid redundant work
during rapid filter changes
First full recalibration since 2026-05-24. Corpus is 1,726,757 PSKR
samples (50/144/432 MHz) plus 74,072 contacts joined to hrrr_profiles
(microwave), yielding overrides for 12 bands.
The 6m fit moved most, and coherently: humidity 0.125 -> 0.204 and pwat
0.120 -> 0.185, paid for by season 0.217 -> 0.147. 6m contributes 1.63M
of the 1.73M PSKR samples, so ten weeks of new spot density pulled the
band off a seasonal prior and onto direct moisture signal. Next largest
is 144 MHz refractivity 0.111 -> 0.053.
75 GHz (n=93) and 122 GHz (n=63) appear in the evidence table but get
no overrides -- they fall under MIN_N_FOR_FIT, which is correct; 75 GHz
reports rho_pr of -0.774 on 93 samples, which is noise, not physics.
Caveat: the refractivity multipliers are numerically unstable. They are
computed as |band_rho_grad| / |ref_rho_grad| against the 10 GHz
reference, whose rho_grad is +0.030 -- an order of magnitude smaller
than the other reference correlations. Two runs minutes apart moved
47 GHz refractivity 0.0688 -> 0.0522 (-24% relative) purely on new rows
landing between queries. The stable factors barely moved. These weights
are live once deployed, so this commit is isolated from the script fixes
to keep it independently revertable.
Two defects surfaced by the 2026-08-01 production run.
The generated report's "Applying these weights" section claimed the
JSON was inert until someone wired it into BandConfig. That stopped
being true when BandWeights landed: BandConfig.weights/1 consults
BandWeights.lookup/1 ahead of the in-source :weights override and the
global @weights default, so band_weights.json is a live scoring input.
Anyone reading the report would have concluded a recalibration run was
a no-op. Replace the paragraph and document the operational half that
was missing entirely -- BandWeights caches into :persistent_term on
first read, so a running node serves the previous weights until it is
redeployed or BandWeights.reset/0 is called.
PSKR_BAND_TO_MHZ was missing 9cm, so every run logged "skipping unknown
PSKR band" and dropped those rows from the corpus accounting. Weight
derivation is unaffected -- 3400 MHz is not in PSKR_BANDS, so it still
routes to the contacts fit (source=contacts, n=334, unchanged). The
dict already carries other non-routed bands (23cm, 13cm, 3cm); 9cm was
simply absent.
- Add Canadian HRDPS model coverage (to 60°N)
- Document Rust pipeline (prop-grid-rs) and its performance characteristics
- Add .sgrid/.pgrid binary format details
- Note ~6s chain step and 23-band fused pass
- Remove Nx/Axon/ExLA ML model references (scaffolding was removed)
- Update roadmap: drop model training, add PSKR calibration, propagation alerts
- Fix factor count (was 9, is 10) in roadmap item
Four optimisations targeting the 11.4s chain-step critical path:
1. Drop legacy .mp.gz dual-write (~2.5-2.8s, 24% of step)
The Elixir reader prefers .sgrid; the chunked gzip+msgpack write was
pure overhead on the Rust write path. Removed scalar_future from both
run_chain_step and run_analysis_step.
2. Parallelize wgrib2 decodes (~0.5-1s)
sfc and prs decode ran serially (await sfc, then await prs). Both now
spawn concurrently on the blocking pool.
3. Overlap sfc decode with prs fetch (~1-2s)
sfc (9 messages) resolves before prs (39 messages). Start the sfc
wgrib2 subprocess as soon as the sfc blob lands instead of waiting
for both blobs. The sfc decode hides behind the remaining prs fetch.
4. Cap grib range retries from 5 to 3
2^n exponential backoff meant 1+2+4+8+16 = 31s worst-case per failed
range. Three retries caps at 1+2+4 = 7s. 404s already fail fast.
Also: instrument .sgrid write (observe_stage) so it appears in the
prop_grid_rs_stage_duration_seconds histogram alongside fetch/decode/derive.
Expected mean chain step: ~6-7s (down from 11.4s).