Commit graph

20 commits

Author SHA1 Message Date
7ab2aa8a11
feat(rover-planning): row click opens cached path detail, not /path recompute
New /rover-planning/:id/paths/:path_id route renders the worker-stored
result map directly: geometry, link-budget components, propagation
score, computed_at. The 'Recompute live' button hands off to /path
when the operator wants HRRR-driven loss against current weather.
2026-05-03 14:19:31 -05:00
fe2024275b
fix(rover-planning): async reconcile + strict band match in path worker
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.
2026-05-03 14:16:48 -05:00
1d4b2aeedb
fix(rover-planning): edit-mode remove_station deletes only the targeted row
Clicking the trash icon on a stationary station in :edit mode before
any other phx-change had fired emptied the form's stations params,
which combined with cast_assoc(on_replace: :delete) wiped EVERY
existing station instead of just the one the user clicked. Fall back
to rebuilding the params from the persisted mission's stations when
the form's current params don't already carry them, then delete only
the targeted index.
2026-05-03 14:11:38 -05:00
2c88fba1df
feat(rover-planning): multi-band missions + smart reconcile
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).
2026-05-03 14:08:49 -05:00
b247e8a719
feat(rover-planning): warn when new site overlaps an existing 6-char grid
The add-rover-site form now resolves the input on every change and, if
any existing rover-location shares the same 6-char Maidenhead cell
(~5 km), surfaces a 'Already saved as' link to that location's detail
page. Avoids users accidentally duplicating a site they (or another
operator) already added.
2026-05-03 14:00:47 -05:00
23d002e8e0
feat(rover-planning): cache link-budget loss components per path
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.
2026-05-03 13:58:18 -05:00
327b4fc561
feat(rover-planning): inline per-group delete, drop standalone site list
The 'Rover sites' card no longer renders the long list of every site —
each rover-location now appears once as a path-profile group heading,
which carries its own delete button next to the n/m progress text. The
add-site form stays in place.
2026-05-03 13:54:46 -05:00
e84f28643f
fix(rover-planning): truncate long rover-site notes on a single row 2026-05-03 13:48:02 -05:00
39c4c287a5
feat(rover-planning): show callsign + grid (no bare coords) in stationary list
The Stationary-stations summary was rendering whatever the user typed
verbatim ('aa5c 33.1889, -96.4517'). Switch to a pair of helpers that
uppercase the callsign (covering legacy mixed-case rows) and synthesize
a 6-char Maidenhead grid from lat/lon when no grid was stored. Bare
coordinates no longer appear in this list — the grid carries the same
location info in a more amateur-radio-native form.
2026-05-03 13:41:53 -05:00
210e941db8
feat(rover-planning): propagation score column + linked location heading
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.
2026-05-03 13:35:54 -05:00
d3b96725c8
fix(rover-planning): two-line station, drop noise columns, fix verdict badge
Path table now shows callsign on top with grid muted below, and the
detail-only Min clearance / Diffraction columns are gone. The verdict
badge matched lowercase 'clear/blocked/marginal' but the worker stores
uppercase 'CLEAR/BLOCKED/FRESNEL_PARTIAL/FRESNEL_MINOR' from
TerrainAnalysis, so every cell silently rendered empty. Updated the
guard clauses to the actual upstream strings and added Fresnel variants.
2026-05-03 13:26:57 -05:00
08ac76a4b8
refactor(rover): pattern-match-first event handlers + missing test branches
show.ex (rover-planning) and show.ex (rover-locations) auth + delete
flows go from nested case/case/case to a `with` ladder fed by a single
`authenticated/1` clause that returns `{:ok, user}` or
`{:error, :unauthenticated}`. The else block enumerates the small set
of failure tuples instead of rebuilding nested error returns.

Other idiomatic-Elixir tightening:
- `progress_summary/1` reduces with a `tally_path/2` multi-clause
  helper (status as a head pattern, not three Enum.count passes).
- `paths_by_rover_location/1` extracts `group_to_pair/1`,
  `station_position/1`, and `group_lat/1` so each transformation is a
  pattern match instead of an anonymous fn with `&& fallbacks`.
- `error_summary/1` is two clauses (empty vs populated errors)
  instead of a pipe-into-case.
- Drag-to-edit show.ex consolidates the working-coords assigns into
  `assign_working_coords/3`.

Bug fix uncovered while writing tests: `add_station` on a fresh
/rover-planning/new form was a visual no-op on the first click —
cast_assoc replaced the unseeded default Station struct with the new
params row instead of appending. Now the initial changeset is built
from `%{"stations" => %{"0" => %{"position" => "0"}}}` so add_station
appends from the get-go. Regression test added at
test/microwaveprop_web/live/rover_planning_live_test.exs.

New test branches:
- form: add_station appends on first click + remove_station drops
  the targeted row.
- rover-planning show: add_rover_site whitespace-input rejection,
  delete_rover_site permission denial.
- rover-locations show: save_edit rejects unauthenticated drivers.

Suite: 3228 tests, 0 failures. Credo strict: 0 issues.
2026-05-03 13:01:36 -05:00
8fd9759e4e
feat(rover-planning): add/remove rover sites inline on the show page
A new "Rover sites" section between Stationary stations and Path
profiles lists the candidate rover locations the mission scores
against, with:

- An add form that takes the same flexible input as station inputs
  (callsign, Maidenhead grid, or `lat,lon`); on submit it creates a
  global :good rover-location and re-runs the path matrix.
- A per-row trash button visible to the location's owner / admins
  that deletes the location and re-runs the matrix.

`RoverPlanning.candidate_rover_locations/1` is now public so the show
page can list exactly what the worker enqueues against. Add/remove
both call `replace_mission_paths/1` so the matrix stays consistent
with the rover-site set.

Anonymous visitors see a sign-in prompt instead of the form.
2026-05-03 12:47:07 -05:00
5c3a791665
fix(rover-planning): derive grid from lat/lon for coord-only stations
When a user types raw "lat,lon" coordinates into a station input,
LocationResolver populates only `lat` / `lon` — `callsign` and `grid`
stay blank. The path-profiles "Station" cell was falling through to
"33.189, -96.452" instead of showing a useful grid.

Now `station_label/1` (display) and `station_endpoint/1` (path-URL
destination) derive a 6-/8-char Maidenhead grid from the stored
lat/lon when no grid is on the row, matching the rest of the page's
grid-first presentation.
2026-05-03 12:43:32 -05:00
dbd4a78502
feat(rover-planning): clickable rows + callsign·grid labels + 10-char heading grid
- Rows in each rover-location group are now phx-click handlers that
  navigate to /path with source (rover-loc as 10-char Maidenhead grid),
  destination (callsign > grid > coords), band, and rover/station
  heights pre-filled — one click takes you to a ready-to-compute Path
  Calculator screen.
- Station label now shows "AA5C · EM12kp" when both callsign and grid
  resolved, instead of falling back to lat/lon. Bare-grid and
  bare-coord stations keep their existing labels.
- Group heading uses Maidenhead.from_latlon(lat, lon, 10) so the
  10-char grid is shown in full instead of being truncated to 6
  characters.
2026-05-03 11:49:18 -05:00
a18586046d
feat(rover-planning): group path profiles by rover location
The flat path table forced you to scan column 1 to mentally group rows
when standing at a specific rover spot. Each rover location now gets
its own card with a heading (grid + lat/lon + notes) and an inner
station-by-station table, so the paths to the fixed stations from the
spot you're at are read in one place.

Sorted by latitude (north → south) so the same location keeps the same
slot across re-renders. `rover_label/1` is removed (its content now
lives in the group heading).
2026-05-03 11:34:36 -05:00
cff0c6775d
fix(rover-planning): suppress fresh-station error + tolerate integer JSONB values
- add_station no longer seeds `"input" => ""` in params, so used_input?/1
  treats the new row as untouched and hides the validate_required error
  until the user types.
- Show page's format_distance/format_db/format_meters helpers coerce with
  `* 1.0` because JSONB returns bare zeros as integers, which crashed
  Float.round/2 (FunctionClauseError on /rover-planning/:id render).
- Lat/lon row uses items-start + mt-6 trash so the inputs stay aligned
  even when the callsign cell grows from validation feedback.
2026-05-03 11:29:57 -05:00
eca463add1
fix(rover-planning): keep only_known_good checked across phx-change events
The hidden 'false' shadow input was rendered after the checkbox, so when
both were submitted the 'false' value overwrote the checkbox 'true' in
the resulting params map. Move the hidden input before the checkbox so
the checked value wins (matching the core_components checkbox pattern).
2026-05-03 11:19:14 -05:00
209244f364
refactor(rover-locations): rename status atoms ideal→good, off_limits→bad
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
2026-05-03 11:13:18 -05:00
d1a5442afb
feat(rover-planning): /rover-planning page with path-profile worker
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.
2026-05-03 11:04:11 -05:00