- Replace Module.safe_concat/1 with PID-based names in IemRateLimiterTest
and AggregatorTest (8+6 tests) to avoid binary_to_existing_atom errors
after Elixir 1.20 upgrade
- Fix ContactLive ShowCoverageTest assertions (3 tests) that referenced
removed template text ('in queue') and incompatible weather_status
- Use conn.remote_ip instead of init_test_session for internal_network?
tests since the store_remote_ip plug uses atom keys
- accounts.ex: replace if with with-chain in get_user_by_email_and_password
for explicit pattern matching on User struct + valid_password? result.
Collapse nested case into with in create_api_token.
- map_live.ex: extract connected? PubSub subscription block from mount into
named subscribe_if_connected/1 function (cleaner than inline if with _ =).
Deduplicate identical select_time + set_selected_time handlers into shared
handle_set_time/2 helper. Extract toggle_grid + toggle_radar into shared
handle_toggle/3 helper (functions-as-values pattern for assign key).
- Fix Module.safe_concat -> Module.concat in tests with dynamic process names
(safe_concat calls binary_to_existing_atom, but test names are newly generated)
- Fix PSKR AggregatorTest sandbox ownership by switching to async: false
- Fix MapLiveTest assert_patch: regex unsupported in LiveView 1.2, use string match
- Fix WeatherMapLiveTest toggle_grid: assert on checked attribute, not data-grid
- Fix BeaconLive.Index leaking unapproved beacons: wire data_provider to approved_beacons_query
- Fix ContactLive.Index leaking private contacts: wire data_provider with visibility filter
- Fix RecalibratorTest: train() expects factor vectors, not {vector, datetime, band} tuples
- Fix toggle_sort: compare current_field to new_field, not current_order
- Fix internal_network?: handle both atom and string session keys from Plug sessions
Test results: 3979/4001 -> 3997/4001 (18 previously-failing tests now pass)
- CacheTest: fix sweep timing race by using negative TTL (-1)
instead of positive TTL (1) for already-expired entries
- ScoreCache: replace ETS match-spec DateTime comparisons with
:ets.foldl + DateTime.compare — DateTime structs are maps in
Elixir >= 1.15 and ETS can't compare maps with :< / :> guards
- Accounts: drop unsupported returning: true on delete_all,
return [] for expired tokens list
- Backtest: catch ArgumentError from String.to_existing_atom
for unknown feature names, preserving the helpful Mix.Error
- ContactLive IndexTest: invalidate monthly_bars cache before
assertion so test data is visible
- RoverLocationsLive MapTest: invalidate cached points before
assertion
- StatusLiveTest: add DB cleanup in setup to reduce test
interference; parameterize NARR candidate coordinates
HRRR publishes f21-f48 at 3-hourly intervals in addition to the
hourly f01-f18 we already fetch. This extends the grid_tasks seed
from 19 rows (1 analysis + 18 forecast) to 29 rows (1 analysis +
18 hourly + 10 3-hourly), and bumps the UI forecast window
constant from 18h to 48h so the map timeline and path calculator
forecast chart automatically show the extended horizon.
No Rust logic changes needed — the pipeline is data-driven and
u8 forecast_hour already handles f48.
- Update .tool-versions to elixir 1.20.0-otp-29 / erlang 29.0.1
- Update all hex dependencies
- Remove unused aliases in path_compute.ex and rover_planning_live/show.ex
- Fix Oban unique states to include :suspended (use :incomplete group)
Two regressions from the recent performance commits:
1. ScoresController.encode_binary/1 (9cce257d): Enum.reduce produces
iolists, but <<lats::binary>> requires actual binaries. Caused HTTP
500 on every /scores/cells request when score files exist on disk,
making the entire propagation heatmap overlay invisible.
2. ScoreCache.valid_times/1 (15f4175c): Pipe syntax feeds @table as
the first argument to :ets.foldl/3, creating a 4-argument call
that crashes at runtime.
- Radio.group_reciprocals: replace O(n²) nested Enum.filter with O(n)
Enum.group_by using a hash key (sorted station pair + band + hour)
- contact_live/index.ex: cache monthly bars Repo.all(Contact) query
with 60s TTL
- ScoreCache.valid_times: replace :ets.select with :ets.foldl to
avoid intermediate list allocation
- Accounts.list_users: add limit(100) to unbounded query
- Accounts.backfill_missing_home_qth: batch individual Oban.insert
calls into single Oban.insert_all
- pending_edit_for_user: add preload for [:user, :contact] to avoid
lazy-loaded N+1 when templates access those associations
- ScoresController.encode_binary: combine 3 Enum.map passes into a
single Enum.reduce, halving list traversals for score binary encoding
- find_duplicate_contact: push station-pair/grid matching into SQL
WHERE clause so dedup uses the DB index instead of loading all
matching rows + Enum.find in Elixir
- rover_planning_live/show.ex: use pre-loaded @rover_sites assign
instead of Repo.all(Location) on every keystroke
- rover_locations_live/map.ex: cache locations query with 30s TTL
(was loading + JSON-encoding all good locations in every mount)
- status_live.ex: consolidate 5 separate stat fetches into single
cached blob, so PubSub-triggered refreshes hit cache instead of
running 10+ DB queries each time
- NexradCache: replace :ets.tab2list() with foldl to avoid ~1.3 GB heap
allocation on full cache (OOM risk)
- GridCache: replace :ets.select full-table copies with foldl in
ets_latest_valid_time and ets_prune_keep_latest
- Weather.backfill_hrrr_batch: batch N individual UPDATEs into single
VALUES-based SQL statement eliminating ~500 round-trips per batch
- Add 9 missing database indexes across 6 tables for hot-path queries
(contacts, contact_edits, hrrr_profiles, hrrr_native_profiles,
grid_tasks, rover_mission_paths, fixed_stations, beacons)
- Mailer: apply_defaults/1 sets From and Reply-To headers
- AboutLive: content rendering, empty stats, and contact count display
- Fix has_many :beacons association (wrong module path)
- Fix router pipeline order: serve_markdown before accepts,
after secure headers so markdown responses get security headers
- Fix notify_listener_test Process.sleep regression
- Update findings.md test coverage gap status
- grid.ex: memoize conus_points/0 with module attribute (was ~94k recompute per call)
- path_compute.ex: reduce 6 list traversals to single Enum.reduce pass
- radio.ex + application.ex: fix unused variable e and redundant case warnings
- priv: add 5 partial indexes on (qso_timestamp) for enrichment queries (weather, hrrr,
terrain, iemre, radar) matching the WHERE status IN ('pending','failed') AND pos1 IS NOT NULL
pattern used by contacts_needing_enrichment
- findings.md: mark all performance hotspots and corresponding items as fixed
- Replace deprecated xref:exclude with elixirc_options in vendor/oban_pro
- Remove unused require Logger from 8 files
- Pin bitstring size variables with ^ in simple_packing, wgrib2,
complex_packing, section, nexrad_client, mqtt, and radar worker
- Remove dead defp clauses (format/1 nil, depression/2 nil)
- Rename Buildings.Parser type record/0 -> building_record/0 to avoid
overriding built-in type
- Remove redundant catch-all in candidate_detail.ex
- Simplify contact_live/show.ex conditional based on type narrowing
- Fix DateTime.from_iso8601 return pattern in vendor/oban_web
- Upgrade phoenix_live_view to 1.1.31
- Drop --warnings-as-errors from precommit alias; known false positives
from @after_verify-generated code in Elixir 1.20.0-rc.6 + PLV 1.1.x
- Add credo --strict to precommit as replacement static-analysis gate
Replace the two-script Python pipeline (analysis report + Elixir-source
emitter) with a single `scripts/recalibrate.py` that fits per-band
weights from PSKR spot density (VHF/UHF) and contacts↔HRRR correlations
(microwave), writing `priv/algo/band_weights.json` as a machine-readable
artifact. A new Elixir BandWeights module loads this JSON once via
`:persistent_term` cache; BandConfig.weights/1 consults it before falling
back to in-source overrides or global defaults. The script never touches
Elixir source — recalibration is now `python3 scripts/recalibrate.py`
followed by an app restart.
Show on /weather where the detected duct geometry traps each microwave
band. Overview mode bins cells by their lowest trapped frequency
(Bean & Dutton cutoff); band-picker mode masks cells whose duct doesn't
reach the selected band. Cutoff is pre-computed per cell in both
writers (Elixir derive + Rust derive_row reads best_duct_freq_ghz from
CellValues), persisted to ScalarFile, and shipped to the JS hook via
the existing binary cell pack. Also cleans up six pre-existing
length/1 credo warnings in unrelated test files.
format_number rounds to 1 decimal, so the tiny per-km coefficients
(o2_db_km: 0.007 at 10 GHz, gas rate ~0.007 dB/km) all displayed as
'0.0'. Added format_small with 4-decimal precision for those fields.
POST /api/v1/beacon-monitor/measurements accepts one measurement per
integration window from a propmonitor client, authenticated by the
BeaconMonitor token. Records noise floor, signal peak/avg dBFS, SNR,
signal-active-fraction, gain, and frequency for later correlation
against weather and propagation scores.
Beacon UUID must resolve to an approved + on-the-air beacon (404
otherwise — the client drops 404s without retry). Retries with the
same (monitor, beacon, measured_at) are idempotent: a unique index
makes the second insert a 409. Every accepted upload stamps the
monitor's last_seen_at — measurements *are* the heartbeat.
MonitorAuth is a separate plug from the user API-token Auth plug;
monitors are not users. The rate limiter buckets each monitor by id so
retry storms after a 5xx don't burn the shared anon-IP bucket.