fix(weather): drop valid_times older than one hour from the timeline

The HRRR scalar cache can hold a previous run's forecast hours past
their useful life — the timeline was offering "current conditions"
points that were 2-3 hours old, which is more misleading than helpful
on a map labeled "Now".

Filter Weather.available_weather_valid_times() through a `now - 1h`
cutoff in mount, :weather_updated, and :propagation_pipeline_progress
so the timeline only ever shows the most recent analysis hour plus
forecast hours.
This commit is contained in:
Graham McIntire 2026-04-29 14:03:55 -05:00
parent b66024e0c3
commit 9e4ca154ff
No known key found for this signature in database
GPG key ID: F4ABF488E6029E59
2 changed files with 36 additions and 3 deletions

View file

@ -27,7 +27,7 @@ defmodule MicrowavepropWeb.WeatherMapLive do
# the LiveView socket join timeout. The client now paints weather
# through a tile endpoint, so mount only needs the selected layer
# and valid_time metadata.
valid_times = Weather.available_weather_valid_times()
valid_times = recent_valid_times(Weather.available_weather_valid_times())
initial_vt = pick_initial_valid_time(valid_times)
{:ok,
@ -65,6 +65,15 @@ defmodule MicrowavepropWeb.WeatherMapLive do
Enum.min_by(valid_times, fn vt -> abs(DateTime.diff(vt, now, :second)) end)
end
# Drop valid_times older than one hour ago. The HRRR scalar cache can
# hold the previous run's forecast hours past their useful life;
# showing 3-hour-old "current conditions" on the map is more
# misleading than helpful.
defp recent_valid_times(valid_times) do
cutoff = DateTime.add(DateTime.utc_now(), -3600, :second)
Enum.filter(valid_times, fn vt -> DateTime.compare(vt, cutoff) != :lt end)
end
@impl true
def handle_event("select_layer", %{"layer" => layer_id}, socket) do
if MapLayers.valid_id?(layer_id) do
@ -122,7 +131,7 @@ defmodule MicrowavepropWeb.WeatherMapLive do
@impl true
def handle_info({:weather_updated, _valid_time}, socket) do
valid_times = Weather.available_weather_valid_times()
valid_times = recent_valid_times(Weather.available_weather_valid_times())
# Keep whatever the user has scrubbed to if it's still available,
# otherwise fall back to the latest on-disk time (GridCache-backed
@ -152,7 +161,7 @@ defmodule MicrowavepropWeb.WeatherMapLive do
# so the timeline picks up the extra hour, but keep the currently
# selected time where it is.
def handle_info({:propagation_pipeline_progress, _progress}, socket) do
valid_times = Weather.available_weather_valid_times()
valid_times = recent_valid_times(Weather.available_weather_valid_times())
if valid_times == socket.assigns.valid_times do
{:noreply, socket}

View file

@ -75,6 +75,30 @@ defmodule MicrowavepropWeb.WeatherMapLiveTest do
end)
end
test "mount drops valid_times older than one hour ago", %{conn: conn} do
now =
DateTime.utc_now()
|> DateTime.truncate(:second)
|> Map.put(:minute, 0)
|> Map.put(:second, 0)
stale_t = DateTime.add(now, -2 * 3600, :second)
recent_t = DateTime.add(now, -30 * 60, :second)
future_t = DateTime.add(now, 3 * 3600, :second)
Enum.each([stale_t, recent_t, future_t], &write_profile(&1, 33.0, -97.0))
{:ok, _lv, html} = live(conn, ~p"/weather")
stale_iso = DateTime.to_iso8601(stale_t)
recent_iso = DateTime.to_iso8601(recent_t)
future_iso = DateTime.to_iso8601(future_t)
refute html =~ stale_iso, "stale valid_time #{stale_iso} should be filtered out"
assert html =~ recent_iso, "recent valid_time #{recent_iso} should be present"
assert html =~ future_iso, "future valid_time #{future_iso} should be present"
end
test "mount defaults to the valid_time closest to now, not the latest forecast hour",
%{conn: conn} do
base =