diff --git a/lib/microwaveprop_web/live/skewt_live.ex b/lib/microwaveprop_web/live/skewt_live.ex index 124fe6b7..920fbb6e 100644 --- a/lib/microwaveprop_web/live/skewt_live.ex +++ b/lib/microwaveprop_web/live/skewt_live.ex @@ -30,7 +30,8 @@ defmodule MicrowavepropWeb.SkewtLive do selected_valid_time: nil, profile: nil, derived: nil, - svg: nil + svg: nil, + loading?: false )} end @@ -52,8 +53,55 @@ defmodule MicrowavepropWeb.SkewtLive do {:noreply, push_patch(socket, to: ~p"/skewt?#{params}")} end + @impl true + def handle_async(:load_skewt, {:ok, {:ok, result}}, socket) do + {:noreply, + assign(socket, + location: result.location, + error: nil, + valid_times: result.valid_times, + selected_valid_time: result.selected_valid_time, + profile: result.profile, + derived: result.derived, + svg: result.svg, + loading?: false + )} + end + + def handle_async(:load_skewt, {:ok, {:error, reason}}, socket) do + {:noreply, + assign(socket, + location: nil, + error: reason, + valid_times: [], + selected_valid_time: nil, + profile: nil, + derived: nil, + svg: nil, + loading?: false + )} + end + + def handle_async(:load_skewt, {:exit, reason}, socket) do + require Logger + + Logger.warning("SkewtLive async exit: #{inspect(reason)}") + + {:noreply, + assign(socket, + error: "Sorry, something went wrong loading this profile. Try again.", + loading?: false + )} + end + # ── Refresh logic ────────────────────────────────────────────────── + # The page chrome (header + search bar) renders synchronously on + # mount; everything else (geocoding, HRRR file/DB read, sounding + # parameter derivation, SVG render) lands via `:load_skewt` async. + # Repeated calls under the same key cancel any in-flight task, so + # rapid URL patches don't race. + defp refresh(socket, "", _time) do assign(socket, query: "", @@ -63,48 +111,45 @@ defmodule MicrowavepropWeb.SkewtLive do selected_valid_time: nil, profile: nil, derived: nil, - svg: nil + svg: nil, + loading?: false ) end defp refresh(socket, query, requested_time) do + socket + |> assign( + query: query, + error: nil, + loading?: true + ) + |> start_async(:load_skewt, fn -> resolve_and_load(query, requested_time) end) + end + + defp resolve_and_load(query, requested_time) do case SkewtLocationResolver.resolve(query) do {:ok, %{lat: lat, lon: lon} = location} -> valid_times = available_valid_times(lat, lon) - chosen = pick_valid_time(valid_times, requested_time) {profile, derived, svg} = case chosen do - nil -> - {nil, nil, nil} - - time -> - load_profile(time, lat, lon) + nil -> {nil, nil, nil} + time -> load_profile(time, lat, lon) end - assign(socket, - query: query, - location: location, - error: nil, - valid_times: valid_times, - selected_valid_time: chosen, - profile: profile, - derived: derived, - svg: svg - ) + {:ok, + %{ + location: location, + valid_times: valid_times, + selected_valid_time: chosen, + profile: profile, + derived: derived, + svg: svg + }} {:error, reason} -> - assign(socket, - query: query, - location: nil, - error: reason, - valid_times: [], - selected_valid_time: nil, - profile: nil, - derived: nil, - svg: nil - ) + {:error, reason} end end @@ -284,6 +329,13 @@ defmodule MicrowavepropWeb.SkewtLive do <% end %> + <%= if @loading? do %> +
+ + Resolving location and loading HRRR profile… +
+ <% end %> + <%= if @location do %>
{@location.label} diff --git a/test/microwaveprop_web/live/skewt_live_test.exs b/test/microwaveprop_web/live/skewt_live_test.exs index 5ee77191..229c1d3c 100644 --- a/test/microwaveprop_web/live/skewt_live_test.exs +++ b/test/microwaveprop_web/live/skewt_live_test.exs @@ -12,17 +12,29 @@ defmodule MicrowavepropWeb.SkewtLiveTest do refute html =~ "viewBox=\"0 0 720" end - test "renders the resolved location label when given a grid square", %{conn: conn} do + test "initial render with a URL query shows the page chrome + a loading indicator", %{conn: conn} do + # The URL parameter triggers an async resolve. The page must + # render the chrome (header, search bar) and a loading state + # synchronously — *before* the geocoder / HRRR fetch returns. {:ok, _view, html} = live(conn, ~p"/skewt?q=EM12kp") - # The grid is resolved client-side from the URL — even when no - # HRRR profile happens to be on disk for this lat/lon, the - # location header should still surface so the user knows their - # input was accepted. + assert html =~ "Skew-T-Log-P" + assert html =~ ~s|placeholder="EM12kp| + assert html =~ "Resolving location" + # Location label has not arrived yet — that's the whole point. + refute html =~ "EM12KP" + end + + test "after the async completes the resolved location label appears", %{conn: conn} do + {:ok, view, _html} = live(conn, ~p"/skewt?q=EM12kp") + + # Wait for the :load_skewt async to finish, then re-render. + html = render_async(view) + assert html =~ "EM12KP" - # Lat/lon get formatted to 3 decimals. assert html =~ "32.646" assert html =~ "-97.125" + refute html =~ "Resolving location" end test "submitting the form patches the URL with the query", %{conn: conn} do @@ -30,8 +42,7 @@ defmodule MicrowavepropWeb.SkewtLiveTest do view |> form("form", q: "EM12") |> render_submit() - # After patch, the location label appears on the page. - assert render(view) =~ "EM12" + assert render_async(view) =~ "EM12" end end end