From d4b849f9766f396c7c93cfc4a7f64e1c987d09ee Mon Sep 17 00:00:00 2001 From: Graham McIntire Date: Mon, 13 Apr 2026 13:44:25 -0500 Subject: [PATCH] Show ERA5 fallback on contact detail, fix status page ERA5 progress MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Contact detail page now renders an atmospheric profile section backed by HRRR when available and ERA5 (reanalysis) otherwise, labeled with the data source. When both sources are missing and HRRR is known-unavailable (pre-2014 contacts), the Era5FetchWorker is enqueued so the month-tile backfill gets triggered from a user visit. Status page ERA5 progress previously computed "complete" as (total_contacts - hrrr_unavailable_contacts), which treated every contact with any HRRR status as ERA5-complete — inflating the bar to 76% while era5_profiles had 0 rows. Replaced with a per-candidate EXISTS query against era5_profiles using the same 0.15°/30-min tolerance Weather.find_nearest_era5/3 uses at lookup time. --- .../live/contact_live/show.ex | 95 +++++++++++++++---- lib/microwaveprop_web/live/status_live.ex | 44 +++++++-- .../live/contact_live_test.exs | 38 ++++++++ .../live/status_live_test.exs | 68 +++++++++++++ 4 files changed, 218 insertions(+), 27 deletions(-) create mode 100644 test/microwaveprop_web/live/status_live_test.exs diff --git a/lib/microwaveprop_web/live/contact_live/show.ex b/lib/microwaveprop_web/live/contact_live/show.ex index 2f1392de..db0b8512 100644 --- a/lib/microwaveprop_web/live/contact_live/show.ex +++ b/lib/microwaveprop_web/live/contact_live/show.ex @@ -11,6 +11,7 @@ defmodule MicrowavepropWeb.ContactLive.Show do alias Microwaveprop.Weather alias Microwaveprop.Weather.HrrrClient alias Microwaveprop.Workers.ContactWeatherEnqueueWorker + alias Microwaveprop.Workers.Era5FetchWorker alias Microwaveprop.Workers.HrrrFetchWorker alias Microwaveprop.Workers.SolarIndexWorker alias Microwaveprop.Workers.TerrainProfileWorker @@ -40,6 +41,8 @@ defmodule MicrowavepropWeb.ContactLive.Show do solar: nil, hrrr: nil, hrrr_path: [], + era5: nil, + era5_path: [], iemre: nil, terrain: nil, elevation_profile: nil, @@ -83,6 +86,7 @@ defmodule MicrowavepropWeb.ContactLive.Show do |> start_async(:weather, fn -> load_weather(contact) end) |> start_async(:solar, fn -> load_solar(contact) end) |> start_async(:hrrr_path, fn -> Weather.hrrr_profiles_for_path(contact) end) + |> start_async(:era5_path, fn -> Weather.era5_profiles_for_path(contact) end) |> start_async(:terrain, fn -> Terrain.get_terrain_profile(contact.id) end) |> start_async(:iemre, fn -> load_iemre(contact) end) end @@ -308,6 +312,16 @@ defmodule MicrowavepropWeb.ContactLive.Show do {:noreply, socket} end + def handle_async(:era5_path, {:ok, era5_path}, socket) do + era5 = List.first(era5_path) + contact = socket.assigns.contact + + contact = + if socket.assigns.can_enqueue, do: maybe_enqueue_era5(era5, contact), else: contact + + {:noreply, assign(socket, contact: contact, era5: era5, era5_path: era5_path)} + end + def handle_async(:terrain, {:ok, terrain}, socket) do contact = socket.assigns.contact @@ -326,7 +340,8 @@ defmodule MicrowavepropWeb.ContactLive.Show do {:noreply, assign(socket, :iemre, iemre)} end - def handle_async(slot, {:exit, reason}, socket) when slot in [:weather, :solar, :hrrr_path, :terrain, :iemre] do + def handle_async(slot, {:exit, reason}, socket) + when slot in [:weather, :solar, :hrrr_path, :era5_path, :terrain, :iemre] do Logger.warning("contact hydrate task #{slot} exited: #{inspect(reason)}") {:noreply, socket} end @@ -626,6 +641,33 @@ defmodule MicrowavepropWeb.ContactLive.Show do end end + # ERA5 is a fallback for contacts where HRRR is unavailable (pre-2014 or + # missing from the archive). Only enqueue when HRRR is known-unavailable AND + # we don't already have an ERA5 profile — otherwise we'd waste CDS quota on + # contacts the HRRR path will cover. + defp maybe_enqueue_era5(era5, contact) when not is_nil(era5), do: contact + + defp maybe_enqueue_era5(nil, %{hrrr_status: :unavailable} = contact) do + lat = contact.pos1 && contact.pos1["lat"] + lon = contact.pos1 && (contact.pos1["lon"] || contact.pos1["lng"]) + + if lat && lon do + valid_time = %{DateTime.truncate(contact.qso_timestamp, :second) | minute: 0, second: 0} + + %{ + "lat" => lat, + "lon" => lon, + "valid_time" => DateTime.to_iso8601(valid_time) + } + |> Era5FetchWorker.new() + |> Oban.insert() + end + + contact + end + + defp maybe_enqueue_era5(_nil, contact), do: contact + defp queue_info(counts, queue) do case Map.get(counts, queue, 0) do 0 -> "" @@ -1077,23 +1119,31 @@ defmodule MicrowavepropWeb.ContactLive.Show do
-

HRRR Model Profile

- <%= if @hrrr do %> +

Atmospheric Profile

+ <% profile = @hrrr || @era5 %> + <% profile_source = + cond do + @hrrr -> "HRRR (3 km)" + @era5 -> "ERA5 (0.25°)" + true -> nil + end %> + <%= if profile do %>