fix(weather-map): default timeline cursor to the hour closest to now

Previously the map loaded the latest cached valid_time from GridCache,
which can be any +Nh forecast hour the worker has written while walking
f00-f18. Users expect to see current conditions on load, not a forecast
that's ~11 hours out.
This commit is contained in:
Graham McIntire 2026-04-17 15:57:34 -05:00
parent d22a0bba36
commit c09446a517
No known key found for this signature in database
GPG key ID: F4ABF488E6029E59
2 changed files with 54 additions and 9 deletions

View file

@ -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)}

View file

@ -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()