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.
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).
Renames the rover-location status enum and every label that referenced
it. Existing rows are migrated in place. Also touches the consumers:
- Rover.Location enum + default
- RoverLocationsLive index, status filter, form, show, badges
- Rover.Compute and rover_live (only_ideal_locations → only_good_locations)
- RoverPlanning context candidate filter (status == :good)
- RoverPlanning.Show / Form copy
- All rover-related tests
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.