fix(weather): default selected time to current hour, not clock-closest

Past minute 30 of the wall clock the next forecast hour is closer to
utc_now() than the current hour, so the picker would land on (e.g.)
19:00Z when the user is at 18:43Z. HRRR has 19:00Z, but the HRDPS
chain only seeds the *current* hour (18:00Z), so the dual-source map
showed an empty Canadian half whenever we crossed the half-hour mark.

Switch to "most-recent valid_time <= now", with a fallback to the
closest future when nothing on disk is yet at-or-before now (cold
start). Hoist the picker into a public function so it can be unit-
tested deterministically — anchoring the integration test on
DateTime.utc_now() truncated to the hour would have only verified the
new behavior past minute 30.

/weather-ca delegates to the same picker so the two maps agree on
"current."
This commit is contained in:
Graham McIntire 2026-04-30 13:50:33 -05:00
parent 040ebf9d3d
commit b37692592b
No known key found for this signature in database
GPG key ID: F4ABF488E6029E59
3 changed files with 49 additions and 8 deletions

View file

@ -97,11 +97,11 @@ defmodule MicrowavepropWeb.WeatherCaMapLive do
defp parse_float_param(_value, _min, _max, default), do: default 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 defp pick_initial_valid_time(valid_times) do
now = DateTime.utc_now() MicrowavepropWeb.WeatherMapLive.pick_initial_valid_time(valid_times)
Enum.min_by(valid_times, fn vt -> abs(DateTime.diff(vt, now, :second)) end)
end end
# Don't filter by recency. HRDPS publishes 4×/day with a 3-4h # Don't filter by recency. HRDPS publishes 4×/day with a 3-4h

View file

@ -113,11 +113,25 @@ defmodule MicrowavepropWeb.WeatherMapLive do
defp parse_float_param(_value, _min, _max, default), do: default 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 def pick_initial_valid_time(valid_times, now) do
now = DateTime.utc_now() # Prefer the most recent valid_time that is <= now (the "current
Enum.min_by(valid_times, fn vt -> abs(DateTime.diff(vt, now, :second)) end) # 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 end
# Drop valid_times older than one hour ago. The HRRR scalar cache can # Drop valid_times older than one hour ago. The HRRR scalar cache can

View file

@ -131,6 +131,33 @@ defmodule MicrowavepropWeb.WeatherMapLiveTest do
refute html =~ ~s(data-selected-time="#{future_iso}") refute html =~ ~s(data-selected-time="#{future_iso}")
end 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 test "select_time pushes an overlay refresh for the selected hour", %{conn: conn} do
base = base =
DateTime.utc_now() DateTime.utc_now()