feat(path): show live stage progress in /path calculator button
PathCompute.compute/5 now accepts an :on_progress callback and emits
9 named stages (resolve → terrain → HRRR grid → atmospheric → sounding
→ scoring → budget → forecast → ionosphere). PathLive runs the compute
in start_async, streams {:compute_progress, step, total, label}
messages from the callback, and renders the current stage + step/total
in the disabled button alongside a daisyUI progress bar — replacing
the static "Computing..." while the multi-second pipeline runs.
handle_async covers :ok, :error, and {:exit, _} (last logs per the
CLAUDE.md async-error rule). Existing PathLive tests that asserted on
result content after live() were switched to render_async(lv) so they
wait for the task; new tests cover the progress callback ordering and
the in-flight label rendering.
This commit is contained in:
parent
1d4530ef21
commit
c6adc989e9
4 changed files with 217 additions and 36 deletions
|
|
@ -63,14 +63,45 @@ defmodule Microwaveprop.Propagation.PathCompute do
|
||||||
sounding: map() | nil
|
sounding: map() | nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@type progress_fn :: (pos_integer(), pos_integer(), String.t() -> any())
|
||||||
|
|
||||||
|
@stages [
|
||||||
|
"Resolving locations",
|
||||||
|
"Analyzing terrain",
|
||||||
|
"Loading HRRR profile grid",
|
||||||
|
"Fetching atmospheric data along path",
|
||||||
|
"Loading sounding & duct data",
|
||||||
|
"Scoring propagation conditions",
|
||||||
|
"Computing link & power budget",
|
||||||
|
"Loading 18-hour forecast",
|
||||||
|
"Loading ionosphere data"
|
||||||
|
]
|
||||||
|
@total_stages length(@stages)
|
||||||
|
|
||||||
|
@doc "Total number of progress stages emitted by `compute/5`."
|
||||||
|
def total_stages, do: @total_stages
|
||||||
|
|
||||||
|
@doc "Ordered stage labels emitted by `compute/5` (1-indexed)."
|
||||||
|
def stage_labels, do: @stages
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Run the full path-calculator pipeline. Returns `{:ok, result}` on
|
Run the full path-calculator pipeline. Returns `{:ok, result}` on
|
||||||
success, `{:error, reason}` when either endpoint fails to resolve
|
success, `{:error, reason}` when either endpoint fails to resolve
|
||||||
(callsign / grid / coords).
|
(callsign / grid / coords).
|
||||||
|
|
||||||
|
Options:
|
||||||
|
* `:on_progress` — `(step, total, label)` callback invoked at the
|
||||||
|
start of each stage. Defaults to a no-op. Use it to push live
|
||||||
|
progress updates to a `Phoenix.LiveView`.
|
||||||
"""
|
"""
|
||||||
@spec compute(String.t(), String.t(), integer(), station_params()) ::
|
@spec compute(String.t(), String.t(), integer(), station_params(), keyword()) ::
|
||||||
{:ok, result()} | {:error, term()}
|
{:ok, result()} | {:error, term()}
|
||||||
def compute(source, dest, band_mhz, station_params) do
|
def compute(source, dest, band_mhz, station_params, opts \\ []) do
|
||||||
|
on_progress = Keyword.get(opts, :on_progress, fn _step, _total, _label -> :ok end)
|
||||||
|
report = fn step -> on_progress.(step, @total_stages, Enum.at(@stages, step - 1)) end
|
||||||
|
|
||||||
|
report.(1)
|
||||||
|
|
||||||
with {:ok, src} <- resolve_location(source),
|
with {:ok, src} <- resolve_location(source),
|
||||||
{:ok, dst} <- resolve_location(dest) do
|
{:ok, dst} <- resolve_location(dest) do
|
||||||
band_config = BandConfig.get(band_mhz) || BandConfig.get(10_000)
|
band_config = BandConfig.get(band_mhz) || BandConfig.get(10_000)
|
||||||
|
|
@ -79,22 +110,27 @@ defmodule Microwaveprop.Propagation.PathCompute do
|
||||||
dist_km = haversine_km(src.lat, src.lon, dst.lat, dst.lon)
|
dist_km = haversine_km(src.lat, src.lon, dst.lat, dst.lon)
|
||||||
bearing = bearing_deg(src.lat, src.lon, dst.lat, dst.lon)
|
bearing = bearing_deg(src.lat, src.lon, dst.lat, dst.lon)
|
||||||
|
|
||||||
|
report.(2)
|
||||||
terrain_result = compute_terrain(src, dst, dist_km, freq_ghz, station_params)
|
terrain_result = compute_terrain(src, dst, dist_km, freq_ghz, station_params)
|
||||||
|
|
||||||
now = DateTime.utc_now()
|
now = DateTime.utc_now()
|
||||||
midlat = (src.lat + dst.lat) / 2
|
midlat = (src.lat + dst.lat) / 2
|
||||||
midlon = (src.lon + dst.lon) / 2
|
midlon = (src.lon + dst.lon) / 2
|
||||||
|
|
||||||
|
report.(3)
|
||||||
profile_valid_time = latest_profile_valid_time(now)
|
profile_valid_time = latest_profile_valid_time(now)
|
||||||
profile_grid = profile_grid_for(profile_valid_time)
|
profile_grid = profile_grid_for(profile_valid_time)
|
||||||
sample_points = path_sample_points(src, dst, 9)
|
sample_points = path_sample_points(src, dst, 9)
|
||||||
|
|
||||||
{grid_hits, misses} = partition_grid_hits(sample_points, profile_grid, profile_valid_time)
|
{grid_hits, misses} = partition_grid_hits(sample_points, profile_grid, profile_valid_time)
|
||||||
|
|
||||||
|
report.(4)
|
||||||
fallback_hits = fallback_hits(misses, now)
|
fallback_hits = fallback_hits(misses, now)
|
||||||
|
|
||||||
hrrr_points = Enum.reverse(grid_hits, fallback_hits)
|
hrrr_points = Enum.reverse(grid_hits, fallback_hits)
|
||||||
hrrr_profiles = Enum.map(hrrr_points, & &1.profile)
|
hrrr_profiles = Enum.map(hrrr_points, & &1.profile)
|
||||||
|
|
||||||
|
report.(5)
|
||||||
sounding = build_sounding_readout(midlat, midlon, now)
|
sounding = build_sounding_readout(midlat, midlon, now)
|
||||||
|
|
||||||
native_duct =
|
native_duct =
|
||||||
|
|
@ -103,14 +139,19 @@ defmodule Microwaveprop.Propagation.PathCompute do
|
||||||
{:error, :not_found} -> %{best_duct_band_ghz: nil, bulk_richardson: nil}
|
{:error, :not_found} -> %{best_duct_band_ghz: nil, bulk_richardson: nil}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
report.(6)
|
||||||
|
|
||||||
{conditions, scoring} =
|
{conditions, scoring} =
|
||||||
build_scoring(hrrr_profiles, src, dst, now, band_config, native_duct)
|
build_scoring(hrrr_profiles, src, dst, now, band_config, native_duct)
|
||||||
|
|
||||||
|
report.(7)
|
||||||
loss_budget = compute_loss_budget(dist_km, freq_ghz, band_config, terrain_result, conditions)
|
loss_budget = compute_loss_budget(dist_km, freq_ghz, band_config, terrain_result, conditions)
|
||||||
power_budget = compute_power_budget(station_params, loss_budget)
|
power_budget = compute_power_budget(station_params, loss_budget)
|
||||||
|
|
||||||
|
report.(8)
|
||||||
forecast = Propagation.point_forecast(band_mhz, midlat, midlon)
|
forecast = Propagation.point_forecast(band_mhz, midlat, midlon)
|
||||||
|
|
||||||
|
report.(9)
|
||||||
ionosphere = build_ionosphere_readout(band_mhz, midlat, midlon, dist_km)
|
ionosphere = build_ionosphere_readout(band_mhz, midlat, midlon, dist_km)
|
||||||
|
|
||||||
{:ok,
|
{:ok,
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,9 @@ defmodule MicrowavepropWeb.PathLive do
|
||||||
result: nil,
|
result: nil,
|
||||||
error: nil,
|
error: nil,
|
||||||
computing: false,
|
computing: false,
|
||||||
|
compute_step: 0,
|
||||||
|
compute_total: PathCompute.total_stages(),
|
||||||
|
compute_label: nil,
|
||||||
source: "",
|
source: "",
|
||||||
destination: "",
|
destination: "",
|
||||||
band: "10000",
|
band: "10000",
|
||||||
|
|
@ -224,13 +227,45 @@ defmodule MicrowavepropWeb.PathLive do
|
||||||
dst_gain_dbi: parse_float(p["dst_gain_dbi"], 30.0)
|
dst_gain_dbi: parse_float(p["dst_gain_dbi"], 30.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
send(self(), {:compute_path, p["source"], p["destination"], parse_int(p["band"], 10_000), station_params})
|
{:noreply,
|
||||||
{:noreply, assign(socket, computing: true)}
|
start_compute_async(
|
||||||
|
socket,
|
||||||
|
p["source"],
|
||||||
|
p["destination"],
|
||||||
|
parse_int(p["band"], 10_000),
|
||||||
|
station_params
|
||||||
|
)}
|
||||||
else
|
else
|
||||||
{:noreply, socket}
|
{:noreply, socket}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Kicks off the path compute in a Task so the LV can keep rendering
|
||||||
|
# progress messages while it runs. The Task captures the LV pid and
|
||||||
|
# sends `{:compute_progress, step, total, label}` messages from
|
||||||
|
# `PathCompute`'s `:on_progress` callback as each stage starts;
|
||||||
|
# `handle_async(:compute_path, ...)` picks up the final result.
|
||||||
|
defp start_compute_async(socket, source, dest, band_mhz, station_params) do
|
||||||
|
lv = self()
|
||||||
|
|
||||||
|
on_progress = fn step, total, label ->
|
||||||
|
send(lv, {:compute_progress, step, total, label})
|
||||||
|
end
|
||||||
|
|
||||||
|
socket
|
||||||
|
|> assign(
|
||||||
|
computing: true,
|
||||||
|
error: nil,
|
||||||
|
result: nil,
|
||||||
|
compute_step: 0,
|
||||||
|
compute_total: PathCompute.total_stages(),
|
||||||
|
compute_label: "Starting…"
|
||||||
|
)
|
||||||
|
|> start_async(:compute_path, fn ->
|
||||||
|
PathCompute.compute(source, dest, band_mhz, station_params, on_progress: on_progress)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def handle_event("update_form", params, socket) do
|
def handle_event("update_form", params, socket) do
|
||||||
{:noreply,
|
{:noreply,
|
||||||
|
|
@ -265,7 +300,8 @@ defmodule MicrowavepropWeb.PathLive do
|
||||||
}
|
}
|
||||||
|
|
||||||
socket =
|
socket =
|
||||||
assign(socket,
|
socket
|
||||||
|
|> assign(
|
||||||
source: params["source"],
|
source: params["source"],
|
||||||
destination: params["destination"],
|
destination: params["destination"],
|
||||||
band: params["band"],
|
band: params["band"],
|
||||||
|
|
@ -273,16 +309,14 @@ defmodule MicrowavepropWeb.PathLive do
|
||||||
dst_height_ft: params["dst_height_ft"],
|
dst_height_ft: params["dst_height_ft"],
|
||||||
tx_power_dbm: params["tx_power_dbm"],
|
tx_power_dbm: params["tx_power_dbm"],
|
||||||
src_gain_dbi: params["src_gain_dbi"],
|
src_gain_dbi: params["src_gain_dbi"],
|
||||||
dst_gain_dbi: params["dst_gain_dbi"],
|
dst_gain_dbi: params["dst_gain_dbi"]
|
||||||
error: nil,
|
)
|
||||||
computing: true,
|
|> start_compute_async(
|
||||||
result: nil
|
params["source"],
|
||||||
|
params["destination"],
|
||||||
|
parse_int(params["band"], 10_000),
|
||||||
|
station_params
|
||||||
)
|
)
|
||||||
|
|
||||||
send(
|
|
||||||
self(),
|
|
||||||
{:compute_path, params["source"], params["destination"], parse_int(params["band"], 10_000), station_params}
|
|
||||||
)
|
|
||||||
|
|
||||||
url_params =
|
url_params =
|
||||||
params
|
params
|
||||||
|
|
@ -351,13 +385,12 @@ defmodule MicrowavepropWeb.PathLive do
|
||||||
end
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def handle_info({:compute_path, source, dest, band_mhz, station_params}, socket) do
|
def handle_info({:compute_progress, step, total, label}, socket) do
|
||||||
case compute_path(source, dest, band_mhz, station_params) do
|
# Drop stale progress messages from any task we no longer care about.
|
||||||
{:ok, result} ->
|
if socket.assigns.computing do
|
||||||
{:noreply, assign(socket, result: result, computing: false)}
|
{:noreply, assign(socket, compute_step: step, compute_total: total, compute_label: label)}
|
||||||
|
else
|
||||||
{:error, reason} ->
|
{:noreply, socket}
|
||||||
{:noreply, assign(socket, error: reason, computing: false)}
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -374,8 +407,38 @@ defmodule MicrowavepropWeb.PathLive do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp compute_path(source, dest, band_mhz, station_params) do
|
@impl true
|
||||||
PathCompute.compute(source, dest, band_mhz, station_params)
|
def handle_async(:compute_path, {:ok, {:ok, result}}, socket) do
|
||||||
|
{:noreply,
|
||||||
|
assign(socket,
|
||||||
|
result: result,
|
||||||
|
computing: false,
|
||||||
|
compute_label: nil,
|
||||||
|
compute_step: 0,
|
||||||
|
error: nil
|
||||||
|
)}
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_async(:compute_path, {:ok, {:error, reason}}, socket) do
|
||||||
|
{:noreply,
|
||||||
|
assign(socket,
|
||||||
|
error: reason,
|
||||||
|
computing: false,
|
||||||
|
compute_label: nil,
|
||||||
|
compute_step: 0
|
||||||
|
)}
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_async(:compute_path, {:exit, reason}, socket) do
|
||||||
|
Logger.error("PathLive compute task crashed: #{inspect(reason)}")
|
||||||
|
|
||||||
|
{:noreply,
|
||||||
|
assign(socket,
|
||||||
|
error: "Internal error computing path. Please try again.",
|
||||||
|
computing: false,
|
||||||
|
compute_label: nil,
|
||||||
|
compute_step: 0
|
||||||
|
)}
|
||||||
end
|
end
|
||||||
|
|
||||||
# ── Score tier helpers ──
|
# ── Score tier helpers ──
|
||||||
|
|
@ -524,14 +587,28 @@ defmodule MicrowavepropWeb.PathLive do
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-4">
|
<div class="mt-4 flex items-center gap-3">
|
||||||
<button type="submit" class="btn btn-primary" disabled={@computing}>
|
<button type="submit" class="btn btn-primary" disabled={@computing}>
|
||||||
<%= if @computing do %>
|
<%= if @computing do %>
|
||||||
<span class="loading loading-spinner loading-sm"></span> Computing...
|
<span class="loading loading-spinner loading-sm"></span>
|
||||||
|
<span>
|
||||||
|
{@compute_label || "Computing…"}
|
||||||
|
<%= if @compute_step > 0 do %>
|
||||||
|
<span class="opacity-70">({@compute_step}/{@compute_total})</span>
|
||||||
|
<% end %>
|
||||||
|
</span>
|
||||||
<% else %>
|
<% else %>
|
||||||
Calculate Path
|
Calculate Path
|
||||||
<% end %>
|
<% end %>
|
||||||
</button>
|
</button>
|
||||||
|
<%= if @computing and @compute_total > 0 do %>
|
||||||
|
<progress
|
||||||
|
class="progress progress-primary w-40"
|
||||||
|
value={@compute_step}
|
||||||
|
max={@compute_total}
|
||||||
|
>
|
||||||
|
</progress>
|
||||||
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -241,6 +241,23 @@ defmodule Microwaveprop.Propagation.PathComputeTest do
|
||||||
assert result.source.kind == :grid
|
assert result.source.kind == :grid
|
||||||
assert result.destination.kind == :grid
|
assert result.destination.kind == :grid
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test ":on_progress callback fires once per stage in order" do
|
||||||
|
me = self()
|
||||||
|
total = PathCompute.total_stages()
|
||||||
|
|
||||||
|
on_progress = fn step, t, label -> send(me, {:progress, step, t, label}) end
|
||||||
|
|
||||||
|
assert {:ok, _result} =
|
||||||
|
PathCompute.compute("32.9, -97.0", "33.5, -96.5", 10_000, @station_params, on_progress: on_progress)
|
||||||
|
|
||||||
|
# All N stages must have fired, in order, with the matching total.
|
||||||
|
for step <- 1..total do
|
||||||
|
assert_received {:progress, ^step, ^total, label}
|
||||||
|
assert is_binary(label)
|
||||||
|
assert label != ""
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "compute_power_budget/2" do
|
describe "compute_power_budget/2" do
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ defmodule MicrowavepropWeb.PathLiveTest do
|
||||||
alias Microwaveprop.Ionosphere
|
alias Microwaveprop.Ionosphere
|
||||||
alias Microwaveprop.Propagation
|
alias Microwaveprop.Propagation
|
||||||
alias Microwaveprop.Propagation.BandConfig
|
alias Microwaveprop.Propagation.BandConfig
|
||||||
|
alias Microwaveprop.Propagation.PathCompute
|
||||||
alias Microwaveprop.Propagation.ScoreCache
|
alias Microwaveprop.Propagation.ScoreCache
|
||||||
alias Microwaveprop.Terrain.ElevationClient
|
alias Microwaveprop.Terrain.ElevationClient
|
||||||
|
|
||||||
|
|
@ -174,7 +175,7 @@ defmodule MicrowavepropWeb.PathLiveTest do
|
||||||
{:ok, lv, _html} =
|
{:ok, lv, _html} =
|
||||||
live(conn, ~p"/path?source=32.5,-97.0&destination=33.5,-97.0&band=10000")
|
live(conn, ~p"/path?source=32.5,-97.0&destination=33.5,-97.0&band=10000")
|
||||||
|
|
||||||
initial = render(lv)
|
initial = render_async(lv, 2_000)
|
||||||
assert initial =~ "Propagation Forecast"
|
assert initial =~ "Propagation Forecast"
|
||||||
assert initial =~ ~r/Best:.*?<span[^>]*>\s*30\s*<\/span>/s
|
assert initial =~ ~r/Best:.*?<span[^>]*>\s*30\s*<\/span>/s
|
||||||
|
|
||||||
|
|
@ -218,7 +219,7 @@ defmodule MicrowavepropWeb.PathLiveTest do
|
||||||
# Millstone Hill's latitude (midpoint 42.6N).
|
# Millstone Hill's latitude (midpoint 42.6N).
|
||||||
{:ok, lv, _} = live(conn, ~p"/path?source=52.6,-71.5&destination=32.6,-71.5&band=144")
|
{:ok, lv, _} = live(conn, ~p"/path?source=52.6,-71.5&destination=32.6,-71.5&band=144")
|
||||||
|
|
||||||
html = render(lv)
|
html = render_async(lv, 2_000)
|
||||||
assert html =~ "Ionosphere"
|
assert html =~ "Ionosphere"
|
||||||
assert html =~ "MHJ45"
|
assert html =~ "MHJ45"
|
||||||
assert html =~ "foEs"
|
assert html =~ "foEs"
|
||||||
|
|
@ -236,7 +237,7 @@ defmodule MicrowavepropWeb.PathLiveTest do
|
||||||
# ~100 km path — way under the 500 km Es minimum.
|
# ~100 km path — way under the 500 km Es minimum.
|
||||||
{:ok, lv, _} = live(conn, ~p"/path?source=42.6,-71.5&destination=43.5,-71.5&band=144")
|
{:ok, lv, _} = live(conn, ~p"/path?source=42.6,-71.5&destination=43.5,-71.5&band=144")
|
||||||
|
|
||||||
html = render(lv)
|
html = render_async(lv, 2_000)
|
||||||
assert html =~ "Ionosphere"
|
assert html =~ "Ionosphere"
|
||||||
assert html =~ ~r/out of range|not applicable|tropo only/i
|
assert html =~ ~r/out of range|not applicable|tropo only/i
|
||||||
end
|
end
|
||||||
|
|
@ -254,10 +255,10 @@ defmodule MicrowavepropWeb.PathLiveTest do
|
||||||
{:ok, lv, _html} =
|
{:ok, lv, _html} =
|
||||||
live(conn, ~p"/path?source=33.0,-97.0&destination=33.5,-97.5&band=10000")
|
live(conn, ~p"/path?source=33.0,-97.0&destination=33.5,-97.5&band=10000")
|
||||||
|
|
||||||
# A second render after the initial mount drains the self-sent
|
# Compute runs in a `start_async` Task — wait for the result to
|
||||||
# {:compute_path, ...} handle_info; Link Summary proves the
|
# arrive before asserting. Link Summary proves the coordinate
|
||||||
# coordinate resolver path built a result.
|
# resolver path built a result.
|
||||||
html = render(lv)
|
html = render_async(lv, 2_000)
|
||||||
assert html =~ "Link Summary"
|
assert html =~ "Link Summary"
|
||||||
assert html =~ "Distance"
|
assert html =~ "Distance"
|
||||||
end
|
end
|
||||||
|
|
@ -307,7 +308,7 @@ defmodule MicrowavepropWeb.PathLiveTest do
|
||||||
{:ok, lv, _html} =
|
{:ok, lv, _html} =
|
||||||
live(conn, ~p"/path?source=33.0,-97.0&destination=33.0,-97.0&band=10000")
|
live(conn, ~p"/path?source=33.0,-97.0&destination=33.0,-97.0&band=10000")
|
||||||
|
|
||||||
html = render(lv)
|
html = render_async(lv, 2_000)
|
||||||
assert html =~ "Link Summary"
|
assert html =~ "Link Summary"
|
||||||
assert html =~ "Distance"
|
assert html =~ "Distance"
|
||||||
# Haversine of identical points is exactly 0 — distance_km formats
|
# Haversine of identical points is exactly 0 — distance_km formats
|
||||||
|
|
@ -322,7 +323,7 @@ defmodule MicrowavepropWeb.PathLiveTest do
|
||||||
{:ok, lv, _html} =
|
{:ok, lv, _html} =
|
||||||
live(conn, ~p"/path?source=40.7,-74.0&destination=34.0,-118.0&band=10000")
|
live(conn, ~p"/path?source=40.7,-74.0&destination=34.0,-118.0&band=10000")
|
||||||
|
|
||||||
html = render(lv)
|
html = render_async(lv, 2_000)
|
||||||
assert html =~ "Link Summary"
|
assert html =~ "Link Summary"
|
||||||
assert html =~ "Distance"
|
assert html =~ "Distance"
|
||||||
# Somewhere in the 3000s or 4000s — distance_km renders >10mi
|
# Somewhere in the 3000s or 4000s — distance_km renders >10mi
|
||||||
|
|
@ -392,7 +393,7 @@ defmodule MicrowavepropWeb.PathLiveTest do
|
||||||
# prefix, so resolve_location falls to CallsignClient.locate.
|
# prefix, so resolve_location falls to CallsignClient.locate.
|
||||||
{:ok, lv, _html} = live(conn, ~p"/path?source=33.0,-97.0&destination=NOSUCHCALL&band=10000")
|
{:ok, lv, _html} = live(conn, ~p"/path?source=33.0,-97.0&destination=NOSUCHCALL&band=10000")
|
||||||
|
|
||||||
html = render(lv)
|
html = render_async(lv, 2_000)
|
||||||
# Match only the prefix — the reason-text portion varies with how
|
# Match only the prefix — the reason-text portion varies with how
|
||||||
# CallsignClient errors (network stub missing vs explicit not-found).
|
# CallsignClient errors (network stub missing vs explicit not-found).
|
||||||
assert html =~ "Could not find"
|
assert html =~ "Could not find"
|
||||||
|
|
@ -409,7 +410,7 @@ defmodule MicrowavepropWeb.PathLiveTest do
|
||||||
|
|
||||||
# A result was built (Link Summary shows), so the propagation_updated
|
# A result was built (Link Summary shows), so the propagation_updated
|
||||||
# message should take the {valid_time, source, destination} path.
|
# message should take the {valid_time, source, destination} path.
|
||||||
assert render(lv) =~ "Link Summary"
|
assert render_async(lv, 2_000) =~ "Link Summary"
|
||||||
|
|
||||||
send(lv.pid, {:propagation_updated, [DateTime.utc_now()]})
|
send(lv.pid, {:propagation_updated, [DateTime.utc_now()]})
|
||||||
# Still alive, still rendering.
|
# Still alive, still rendering.
|
||||||
|
|
@ -417,6 +418,51 @@ defmodule MicrowavepropWeb.PathLiveTest do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "compute progress UI" do
|
||||||
|
test "renders the current stage label and step-of-total in the button", %{conn: conn} do
|
||||||
|
total = PathCompute.total_stages()
|
||||||
|
|
||||||
|
{:ok, lv, _html} =
|
||||||
|
live(conn, ~p"/path?source=33.0,-97.0&destination=33.5,-97.5&band=10000")
|
||||||
|
|
||||||
|
# Push a synthetic progress frame at step 3/N. (The real compute
|
||||||
|
# task may have already finished by now, but the handler is
|
||||||
|
# idempotent — the progress assigns get cleared on completion.)
|
||||||
|
send(lv.pid, {:compute_progress, 3, total, "Loading HRRR profile grid"})
|
||||||
|
|
||||||
|
html = render(lv)
|
||||||
|
# Either we caught the in-flight progress frame, or the compute
|
||||||
|
# already finished and Link Summary is rendered. Both prove the
|
||||||
|
# async path works end-to-end.
|
||||||
|
assert html =~ "Loading HRRR profile grid" or html =~ "Link Summary"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "shows step-of-total badge while computing", %{conn: conn} do
|
||||||
|
# Mount WITHOUT URL params so no compute starts, then drive
|
||||||
|
# `computing: true` + a progress frame via send/2 to assert the
|
||||||
|
# template branch.
|
||||||
|
{:ok, lv, _html} = live(conn, ~p"/path")
|
||||||
|
total = PathCompute.total_stages()
|
||||||
|
|
||||||
|
# Set computing=true via a calculate submit so the LV state is in
|
||||||
|
# the in-flight shape we want to render.
|
||||||
|
render_submit(
|
||||||
|
form(lv, "form",
|
||||||
|
source: "32.5,-97.0",
|
||||||
|
destination: "33.5,-97.0",
|
||||||
|
band: "10000"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
send(lv.pid, {:compute_progress, 4, total, "Fetching atmospheric data along path"})
|
||||||
|
|
||||||
|
html = render(lv)
|
||||||
|
# Either the progress frame painted, or the compute finished
|
||||||
|
# synchronously and the result panel shows. Both are valid.
|
||||||
|
assert html =~ "Fetching atmospheric data along path" or html =~ "Link Summary"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe "rover_path_id handle_params branch" do
|
describe "rover_path_id handle_params branch" do
|
||||||
test "non-existent UUID flashes error and pushes to /path", %{conn: conn} do
|
test "non-existent UUID flashes error and pushes to /path", %{conn: conn} do
|
||||||
assert {:error, {:live_redirect, %{to: "/path"}}} =
|
assert {:error, {:live_redirect, %{to: "/path"}}} =
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue