From 9e4ca154ffcaf7196bc297cba01903026a252289 Mon Sep 17 00:00:00 2001 From: Graham McIntire Date: Wed, 29 Apr 2026 14:03:55 -0500 Subject: [PATCH] fix(weather): drop valid_times older than one hour from the timeline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../live/weather_map_live.ex | 15 +++++++++--- .../live/weather_map_live_test.exs | 24 +++++++++++++++++++ 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/lib/microwaveprop_web/live/weather_map_live.ex b/lib/microwaveprop_web/live/weather_map_live.ex index 97b4911f..59c99cae 100644 --- a/lib/microwaveprop_web/live/weather_map_live.ex +++ b/lib/microwaveprop_web/live/weather_map_live.ex @@ -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} diff --git a/test/microwaveprop_web/live/weather_map_live_test.exs b/test/microwaveprop_web/live/weather_map_live_test.exs index c796d456..37c68e2d 100644 --- a/test/microwaveprop_web/live/weather_map_live_test.exs +++ b/test/microwaveprop_web/live/weather_map_live_test.exs @@ -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 =