diff --git a/lib/microwaveprop_web/live/weather_ca_map_live.ex b/lib/microwaveprop_web/live/weather_ca_map_live.ex index 32368051..48b3f0d3 100644 --- a/lib/microwaveprop_web/live/weather_ca_map_live.ex +++ b/lib/microwaveprop_web/live/weather_ca_map_live.ex @@ -97,11 +97,11 @@ defmodule MicrowavepropWeb.WeatherCaMapLive do defp parse_float_param(_value, _min, _max, default), do: default - defp pick_initial_valid_time([]), do: nil - + # Delegate to WeatherMapLive's picker so the two maps agree on what + # "current" means: most-recent <=now, with a fall-through to the + # closest future when nothing is yet at-or-before now. 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) + MicrowavepropWeb.WeatherMapLive.pick_initial_valid_time(valid_times) end # Don't filter by recency. HRDPS publishes 4×/day with a 3-4h diff --git a/lib/microwaveprop_web/live/weather_map_live.ex b/lib/microwaveprop_web/live/weather_map_live.ex index 39bbc9c8..ffbe9f6c 100644 --- a/lib/microwaveprop_web/live/weather_map_live.ex +++ b/lib/microwaveprop_web/live/weather_map_live.ex @@ -113,11 +113,25 @@ defmodule MicrowavepropWeb.WeatherMapLive do defp parse_float_param(_value, _min, _max, default), do: default - defp pick_initial_valid_time([]), do: nil + @doc false + # Public for direct unit testing: anchoring an integration test on a + # truncated `utc_now()` would only assert the new behavior past minute + # 30 of the wall clock. + def pick_initial_valid_time(valid_times, now \\ DateTime.utc_now()) + def pick_initial_valid_time([], _now), 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) + def pick_initial_valid_time(valid_times, now) do + # Prefer the most recent valid_time that is <= now (the "current + # hour" — what HRRR analysis just delivered and what the HRDPS chain + # seeds when fired this hour). Fall back to the closest absolute + # match only when nothing on disk is yet at-or-before now. + past_or_equal = + Enum.filter(valid_times, fn vt -> DateTime.compare(vt, now) != :gt end) + + case past_or_equal do + [] -> Enum.min_by(valid_times, fn vt -> abs(DateTime.diff(vt, now, :second)) end) + list -> Enum.max_by(list, &DateTime.to_unix/1) + end end # Drop valid_times older than one hour ago. The HRRR scalar cache can diff --git a/test/microwaveprop_web/live/weather_map_live_test.exs b/test/microwaveprop_web/live/weather_map_live_test.exs index 0b17375b..be1eb5b5 100644 --- a/test/microwaveprop_web/live/weather_map_live_test.exs +++ b/test/microwaveprop_web/live/weather_map_live_test.exs @@ -131,6 +131,33 @@ defmodule MicrowavepropWeb.WeatherMapLiveTest do refute html =~ ~s(data-selected-time="#{future_iso}") end + test "pick_initial_valid_time prefers the most-recent <=now over a clock-closer future hour" do + # Deterministic unit test against the picker (the integration + # version would only fail past minute 30 of the wall clock). + now = ~U[2026-04-30 18:43:00Z] + current_hour = ~U[2026-04-30 18:00:00Z] + next_hour = ~U[2026-04-30 19:00:00Z] + + # next_hour is closer to `now` (17 min vs 43 min), but the user + # wants the *current hour* so HRRR-analysis-now and HRDPS-seeded- + # for-now align on the dual-source map. + assert MicrowavepropWeb.WeatherMapLive.pick_initial_valid_time( + [current_hour, next_hour], + now + ) == current_hour + end + + test "pick_initial_valid_time falls back to the closest future when nothing is <=now" do + # Cold-start case: no analysis-or-past data on disk yet, only + # forecast hours. The picker should still return something so the + # map is non-blank. + now = ~U[2026-04-30 18:00:00Z] + f1 = ~U[2026-04-30 19:00:00Z] + f6 = ~U[2026-05-01 00:00:00Z] + + assert MicrowavepropWeb.WeatherMapLive.pick_initial_valid_time([f1, f6], now) == f1 + end + test "select_time pushes an overlay refresh for the selected hour", %{conn: conn} do base = DateTime.utc_now()