- 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)
The :terrain queue at 3 slots/pod was sized for the lightweight per-QSO
TerrainProfileWorker, but RoverPathProfileWorker now runs the full
PathCompute pipeline (terrain + 9 HRRR profiles + sounding + ionosphere
+ scoring + loss + forecast) — same heap profile as :propagation, which
sits at 1 slot precisely to avoid OOM.
A backfill_paths flood today queued ~200 rover-path jobs. With 4 hot
pods × 3 slots, the cluster stacked 15 concurrent path-computes and
two pods OOM-killed in a loop, taking the libcluster ring with them
(visible as the "unable to connect" warnings on the surviving pods).
New :rover_path queue at 1 slot/pod caps cluster-wide concurrency at
(hot_replicas + 1 backfill), drains the existing 200-job backlog
inside ~30 min, and keeps :terrain free for the cheap workers it was
sized for.
Three connected changes:
1) Extract PathLive.compute_path/4 + every helper it owned (resolve,
profile grid lookup, sounding/ionosphere readouts, scoring, loss /
power budgets) into Microwaveprop.Propagation.PathCompute. PathLive
now delegates; ~410 lines of dead helpers deleted from PathLive.
2) RoverPathProfileWorker calls PathCompute.compute/4 with the
mission's heights and PathLive's default station params (10 W TX,
30 dBi gains). Stores the full atom-keyed compute output as a
Base64-encoded :erlang.term_to_binary/1 blob alongside the flat
summary fields the rover-planning show table reads. The blob
roundtrips structs and DateTime exactly (Jason.encode would lose
them).
3) PathLive accepts ?rover_path_id=UUID. When set, loads the cached
Path, decodes the term (binary_to_term :safe), assigns it as
@result, and renders normally — no compute_path call. The
rover-planning show table now links rows directly to
/path?rover_path_id=UUID, so a click opens the full Path
Calculator UI from cached data without re-running terrain / HRRR /
sounding lookups.
Bonus prod fixes folded in:
- PathShow's elevation chart attribute (data-* → data-profile JSON)
was crashing the JS hook with 'unexpected character at line 1'.
- Station.changeset now wipes previously-resolved callsign/grid/
lat/lon when the user types into :input — typing 'AA5' early
resolved to a wrong location and locked it; subsequent keystrokes
never re-resolved.
- phx-debounce=600 on the station input so QRZ doesn't get hit on
every keystroke.
Worker now stores the full per-point analysis (beam height,
Fresnel-zone radius, clearance) on Path.result.path_points, and
PathShow renders a real cached page: terrain elevation chart fed by
the same ElevationProfile JS hook /path uses (feet on Y, miles on X,
LOS + Fresnel overlay), distance / bearing / clearance / antenna
heights all in feet, baseline link-budget breakdown, and the cached
midpoint propagation score with computed_at timestamp.
A Recompute live button hands off to /path for the time-varying bits
(current HRRR conditions, scoring breakdown, sounding, ionosphere)
that aren't worth caching.
Two fixes:
1) Form save / rover-site add/remove no longer runs the path-matrix
reconcile inline. update_mission and the show LiveView now
enqueue a single RoverMissionReconcileWorker job and return —
for missions with many rover sites x bands the inline path was
firing hundreds of Multi inserts + Oban.inserts on the request.
2) RoverPathProfileWorker.load_path/1 now strictly matches a
4-tuple (mission_id, rover_location_id, station_id, band_mhz)
with band_mhz as an integer, plus updates the Oban unique key
set to include band_mhz so multi-band missions enqueue per-band
jobs (not just one). Legacy/malformed args are logged and
dropped instead of crashing the queue with MultipleResultsError.
Mission now carries bands_mhz ({:array, :integer}) — operator picks
one or more bands as multi-checkboxes. enqueue_paths_for builds the
cross product (rover x station x band) and persists each tuple as its
own Path row keyed by (mission_id, rover_location_id, station_id,
band_mhz). The path-profile worker reads band_mhz from the path
itself (legacy single-band jobs without band_mhz in args still resolve
to their unique row).
replace_mission_paths/1 is now a thin alias for reconcile_mission_paths/1
which diffs desired vs actual: stale tuples (old band that the user
unchecked, station they removed, rover-site they deleted) get dropped,
new tuples become :pending and enqueue, surviving :complete rows are
left in place — no more wholesale destruction of already-computed
paths on every edit.
The show table gains a Band column, and band_label() in the mission
summary becomes bands_label() (joins the list with commas).
The path-profile worker now also computes the baseline link-loss
budget — free-space path loss, oxygen absorption, baseline H2O loss
(at 7.5 g/m^3 default humidity), plus the already-cached terrain
diffraction — and stores all four numbers in Path.result. The show
page renders the per-row total in a new Loss column. /path still
recomputes against live HRRR weather; this is the offline-readable
cached snapshot.
The path-profile worker now also looks up the propagation grid score for
the path midpoint at the mission's band before flipping status to
:complete, so the row never appears 'done' until both terrain and
propagation prediction have run. The show table renders that score as a
red/yellow/green badge in a new Propagation column.
Each rover-location group heading is now a link to /rover-locations/:id
so the user can jump straight to the spot's detail page from the
mission view.
Adds a new globally-scoped, owner-mutable rover-mission tracker:
- /rover-planning paginated table (LiveTable) with View/Edit/Delete
- /rover-planning/new + /:id/edit form: name, band, antenna heights,
notes, "only check against known good locations" toggle (default on),
and a dynamic list of stationary stations entered as callsigns,
Maidenhead grids, or lat,lon pairs (Station changeset geocodes via
LocationResolver, lat/lon stays editable after add)
- /rover-planning/:id show page renders the station list, scope, and a
matrix of computed path profiles (distance, min clearance, diffraction,
verdict) populated as the worker completes each pairing
After save, RoverPlanning enqueues one RoverPathProfileWorker job per
(rover-location × station) pairing. The worker mirrors PathLive's
synchronous compute (ElevationClient + TerrainAnalysis at the mission's
band + heights) and stores the result on the matching path row.
PubSub broadcast on completion lets the show page live-refresh.
Admins can edit/delete any mission; owners can edit their own.