diff --git a/lib/microwaveprop_web/live/weather_map_live.ex b/lib/microwaveprop_web/live/weather_map_live.ex index e66bef49..ecd78813 100644 --- a/lib/microwaveprop_web/live/weather_map_live.ex +++ b/lib/microwaveprop_web/live/weather_map_live.ex @@ -143,14 +143,25 @@ defmodule MicrowavepropWeb.WeatherMapLive do Phoenix.PubSub.subscribe(Microwaveprop.PubSub, "propagation:pipeline") end - # Hot-path mount: use the cache-only latest_weather_grid for the - # initial payload so mount stays under the LV socket join timeout - # (~10s). Reading and deriving the full 92k-point ProfilesFile - # synchronously pushes mount past that window on a cold pod, - # which makes the LV client fall back to a full page reload. - data = Weather.latest_weather_grid(@initial_bounds) - latest_vt = if data == [], do: Weather.latest_grid_valid_time(), else: hd(data).valid_time + # Default the timeline cursor to the valid_time closest to "now" so + # users land on current conditions, not a +11h forecast hour (which + # the underlying cache can advance to while the worker is walking + # f00→f18). + # + # Hot-path mount: when the closest-to-now time happens to be the + # cached latest, use `latest_weather_grid/1` so mount stays under + # the LV socket join timeout (~10s). A ProfilesFile read for any + # other hour is a single ~2 MB ETF decode, also fine for mount. valid_times = Weather.available_weather_valid_times() + latest_vt = Weather.latest_grid_valid_time() + initial_vt = pick_initial_valid_time(valid_times) + + data = + cond do + is_nil(initial_vt) -> [] + initial_vt == latest_vt -> Weather.latest_weather_grid(@initial_bounds) + true -> Weather.weather_grid_at(initial_vt, @initial_bounds) + end {:ok, assign(socket, @@ -158,10 +169,10 @@ defmodule MicrowavepropWeb.WeatherMapLive do layers: @layers, selected_layer: "refractivity_gradient", initial_data_json: Jason.encode!(data), - valid_time: latest_vt, + valid_time: initial_vt, valid_times: valid_times, initial_valid_times_json: Jason.encode!(Enum.map(valid_times, &DateTime.to_iso8601/1)), - initial_selected_time: latest_vt && DateTime.to_iso8601(latest_vt), + initial_selected_time: initial_vt && DateTime.to_iso8601(initial_vt), initial_utc_clock: Calendar.strftime(DateTime.utc_now(), "%H:%M UTC"), bounds: @initial_bounds, grid_visible: false, @@ -169,6 +180,13 @@ defmodule MicrowavepropWeb.WeatherMapLive do )} end + defp pick_initial_valid_time([]), do: nil + + defp pick_initial_valid_time(valid_times) do + now = DateTime.utc_now() + Enum.min_by(valid_times, fn vt -> abs(DateTime.diff(vt, now, :second)) end) + end + @impl true def handle_event("select_layer", %{"layer" => layer_id}, socket) do {:noreply, assign(socket, :selected_layer, layer_id)} diff --git a/test/microwaveprop_web/live/weather_map_live_test.exs b/test/microwaveprop_web/live/weather_map_live_test.exs index e755bde8..38ff84ae 100644 --- a/test/microwaveprop_web/live/weather_map_live_test.exs +++ b/test/microwaveprop_web/live/weather_map_live_test.exs @@ -75,6 +75,33 @@ defmodule MicrowavepropWeb.WeatherMapLiveTest do end) end + test "mount defaults to the valid_time closest to now, not the latest forecast hour", + %{conn: conn} do + base = + DateTime.utc_now() + |> DateTime.truncate(:second) + |> Map.put(:minute, 0) + |> Map.put(:second, 0) + + # Seed f00 (now) and f11 (+11h). The worker walks forecast hours + # sequentially so `latest_grid_valid_time` can point to f11 even + # though the user expects the map to show "Now" by default. + now_t = base + future_t = DateTime.add(base, 11 * 3600, :second) + + write_profile(now_t, 33.0, -97.0) + write_profile(future_t, 33.0, -97.0) + + {:ok, _lv, html} = live(conn, ~p"/weather") + + now_iso = DateTime.to_iso8601(now_t) + future_iso = DateTime.to_iso8601(future_t) + + # The map element's data-selected-time attribute should be f00, not f11. + assert html =~ ~s(data-selected-time="#{now_iso}") + refute html =~ ~s(data-selected-time="#{future_iso}") + end + test "select_time replaces the rendered weather data with the new hour's grid", %{conn: conn} do base = DateTime.utc_now()