Fix rover planner: manual Calculate button, non-blocking computation

- Add explicit "Calculate Coverage" button instead of auto-triggering
- Move coverage computation to background Task so LV stays responsive
  (users can keep adding/removing stations while it computes)
- Remove all auto-compute triggers from station add/remove/band change
- URL loading resolves stations but doesn't auto-calculate
- Button shows "Finding best roving locations..." spinner while computing
This commit is contained in:
Graham McIntire 2026-04-07 15:53:08 -05:00
parent 961ae0782a
commit a13d5a47f2

View file

@ -80,17 +80,24 @@ defmodule MicrowavepropWeb.RoverLive do
idx = String.to_integer(idx_str)
stations = List.delete_at(socket.assigns.stations, idx)
socket = assign(socket, stations: stations, coverage: [], selected_grid: nil)
if length(stations) >= 2, do: send(self(), :compute_coverage)
socket = push_event(socket, "stations_updated", %{stations: station_data(stations)})
{:noreply, push_url(socket)}
end
def handle_event("select_band", %{"band" => band_str}, socket) do
socket = assign(socket, band: String.to_integer(band_str), coverage: [], selected_grid: nil)
if length(socket.assigns.stations) >= 2, do: send(self(), :compute_coverage)
{:noreply, push_url(socket)}
end
def handle_event("calculate", _params, socket) do
if length(socket.assigns.stations) >= 2 and not socket.assigns.computing do
send(self(), :compute_coverage)
{:noreply, assign(socket, computing: true)}
else
{:noreply, socket}
end
end
def handle_event("select_grid", %{"grid" => grid}, socket) do
detail = Enum.find(socket.assigns.coverage, &(&1.grid == grid))
{:noreply, assign(socket, selected_grid: detail)}
@ -112,7 +119,6 @@ defmodule MicrowavepropWeb.RoverLive do
|> assign(stations: stations, resolving: false)
|> push_event("stations_updated", %{stations: station_data(stations)})
if length(stations) >= 2, do: send(self(), :compute_coverage)
{:noreply, socket}
end
@ -123,7 +129,6 @@ defmodule MicrowavepropWeb.RoverLive do
stations = socket.assigns.stations ++ [station]
socket = assign(socket, stations: stations, resolving: false)
socket = push_event(socket, "stations_updated", %{stations: station_data(stations)})
if length(stations) >= 2, do: send(self(), :compute_coverage)
{:noreply, push_url(socket)}
{:error, _reason} ->
@ -134,11 +139,52 @@ defmodule MicrowavepropWeb.RoverLive do
def handle_info(:compute_coverage, socket) do
stations = socket.assigns.stations
band_mhz = socket.assigns.band
if length(stations) < 2 do
{:noreply, socket}
else
# Run in a background task so the LV stays responsive
pid = self()
Task.start(fn ->
result = do_compute_coverage(stations, band_mhz)
send(pid, {:coverage_result, result})
end)
{:noreply, assign(socket, computing: true)}
end
end
def handle_info({:coverage_result, coverage}, socket) do
stations = socket.assigns.stations
grid_data =
Enum.map(coverage, fn c ->
%{
grid: c.grid,
lat: c.lat,
lon: c.lon,
coverage_score: c.coverage_score,
stations_in_range: c.stations_in_range,
workable_count: c.workable_count,
total_stations: length(stations),
prop_score: c.prop_score,
best_hour: c.best_hour
}
end)
socket =
socket
|> assign(coverage: coverage, computing: false, selected_grid: nil)
|> push_event("coverage_updated", %{grids: grid_data})
{:noreply, socket}
end
defp do_compute_coverage(stations, band_mhz) do
band_config = BandConfig.get(band_mhz) || BandConfig.get(10_000)
max_range = band_config.extended_range_km || 500
socket = assign(socket, computing: true)
# Find the bounding box around all stations, expanded by max range
lats = Enum.map(stations, & &1.lat)
lons = Enum.map(stations, & &1.lon)
@ -180,28 +226,7 @@ defmodule MicrowavepropWeb.RoverLive do
end)
|> Enum.sort_by(& &1.coverage_score, :desc)
# Push to JS for rendering
grid_data =
Enum.map(coverage, fn c ->
%{
grid: c.grid,
lat: c.lat,
lon: c.lon,
coverage_score: c.coverage_score,
stations_in_range: c.stations_in_range,
workable_count: c.workable_count,
total_stations: length(stations),
prop_score: c.prop_score,
best_hour: c.best_hour
}
end)
socket =
socket
|> assign(coverage: coverage, computing: false, selected_grid: nil)
|> push_event("coverage_updated", %{grids: grid_data})
{:noreply, socket}
coverage
end
# ── Coverage Scoring ──
@ -433,16 +458,25 @@ defmodule MicrowavepropWeb.RoverLive do
</div>
<% end %>
<%= if @computing do %>
<div class="text-xs text-center py-3">
<span class="loading loading-spinner loading-sm"></span>
<div class="mt-1">Computing coverage areas...</div>
</div>
<%= if length(@stations) >= 2 do %>
<button
type="button"
phx-click="calculate"
class="btn btn-primary btn-sm w-full"
disabled={@computing}
>
<%= if @computing do %>
<span class="loading loading-spinner loading-sm"></span>
Finding best roving locations...
<% else %>
Calculate Coverage
<% end %>
</button>
<% end %>
<%= if @stations != [] and !@computing and @coverage == [] do %>
<%= if length(@stations) == 1 do %>
<div class="text-xs opacity-50 text-center py-2">
Add at least 2 stations to see coverage
Add at least 2 stations to calculate
</div>
<% end %>