Sync map progress chip to what's actually loadable

The "Updating propagation +Nh" chip was double-misleading in prod:
the label's +Nh was relative to the HRRR run time (which lags wall
clock by ~2h), and the progress broadcast fired before the hour was
fetched + persisted — so the chip could read "+8h" while the map
timeline only extended to +4h.

Reframe the label as "through now" / "through +Nh" / "through Nh ago"
by computing the offset from valid_time → now in PipelineStatus.
running_detail, so it matches the semantic the user reads off the
timeline. Move the PubSub broadcast in PropagationGridWorker to fire
after Propagation.replace_scores/2 succeeds, so the chip only
advances once that forecast hour is readable from ScoresFile.
This commit is contained in:
Graham McIntire 2026-04-15 09:04:54 -05:00
parent 9d2d67305e
commit 11617e730b
No known key found for this signature in database
GPG key ID: F4ABF488E6029E59
4 changed files with 72 additions and 28 deletions

View file

@ -45,16 +45,38 @@ defmodule Microwaveprop.Propagation.PipelineStatus do
@doc """
Format a forecast-progress payload (as broadcast by
`PropagationGridWorker`) into a short detail line for the running
chip ("now", "+3h", ).
chip ("through now", "through +3h", ).
Returns `nil` when the payload has no `forecast_hour` so callers can
The label expresses how far the pipeline has advanced relative to
*now*, not relative to the HRRR run time: the map can scrub the
timeline through this instant. This is the semantic the user expects
when watching the chip advance HRRR lagging the wall clock by
~2 hours means f00's valid_time is actually "2h ago", and "+Nh" in
worker-frame coordinates is Nh-2 in user-frame coordinates.
Returns `nil` when the payload has no `valid_time` so callers can
fall back to the base detail (e.g. "HRRR run") from `current/0`.
"""
@spec running_detail(map() | nil) :: String.t() | nil
def running_detail(%{forecast_hour: 0}), do: "now"
def running_detail(%{forecast_hour: fh}) when is_integer(fh) and fh > 0, do: "+#{fh}h"
def running_detail(%{valid_time: %DateTime{} = valid_time}) do
diff_minutes = DateTime.diff(valid_time, DateTime.utc_now(), :minute)
"through " <> format_offset(diff_minutes)
end
def running_detail(_), do: nil
defp format_offset(diff) when diff >= -30 and diff <= 30, do: "now"
defp format_offset(diff) when diff > 30 do
hours = div(diff + 30, 60)
"+#{hours}h"
end
defp format_offset(diff) do
hours = div(-diff + 30, 60)
"#{hours}h ago"
end
@spec current() :: t()
def current do
case running_workers() do

View file

@ -157,14 +157,6 @@ defmodule Microwaveprop.Workers.PropagationGridWorker do
defp process_forecast_hour(points, run_time, forecast_hour, valid_time) do
label = "f#{String.pad_leading(Integer.to_string(forecast_hour), 2, "0")}"
# Emit progress so the map page's pipeline status chip can show
# which forecast hour is currently being scored ("now", "+1h", …).
Phoenix.PubSub.broadcast(
Microwaveprop.PubSub,
"propagation:pipeline",
{:propagation_pipeline_progress, %{forecast_hour: forecast_hour, valid_time: valid_time}}
)
case timed(label, fn ->
HrrrClient.fetch_grid(points, run_time, forecast_hour: forecast_hour)
end) do
@ -234,6 +226,18 @@ defmodule Microwaveprop.Workers.PropagationGridWorker do
{:ok, count} ->
Logger.info("PropagationGrid: #{label}#{count} scores for #{valid_time}")
warm_cache(valid_time)
# Broadcast progress *after* persistence so the map's
# pipeline chip only advances to "through +Nh" once that
# hour is actually readable from the scores file. Emitting
# this before the fetch would push the chip ahead of the
# map by the full forecast-hour wall time (~10 minutes).
Phoenix.PubSub.broadcast(
Microwaveprop.PubSub,
"propagation:pipeline",
{:propagation_pipeline_progress, %{forecast_hour: forecast_hour, valid_time: valid_time}}
)
:ok
error ->

View file

@ -166,18 +166,34 @@ defmodule Microwaveprop.Propagation.PipelineStatusTest do
end
describe "running_detail/1" do
test "forecast hour 0 renders as 'now'" do
assert PipelineStatus.running_detail(%{forecast_hour: 0}) == "now"
test "valid_time within half an hour of now renders as 'through now'" do
now = DateTime.utc_now()
assert PipelineStatus.running_detail(%{valid_time: now}) == "through now"
assert PipelineStatus.running_detail(%{valid_time: DateTime.add(now, 20 * 60, :second)}) == "through now"
assert PipelineStatus.running_detail(%{valid_time: DateTime.add(now, -20 * 60, :second)}) == "through now"
end
test "positive forecast hours render as +Nh" do
assert PipelineStatus.running_detail(%{forecast_hour: 1}) == "+1h"
assert PipelineStatus.running_detail(%{forecast_hour: 18}) == "+18h"
test "valid_time in the future renders as 'through +Nh'" do
now = DateTime.utc_now()
assert PipelineStatus.running_detail(%{valid_time: DateTime.add(now, 3 * 3600, :second)}) ==
"through +3h"
assert PipelineStatus.running_detail(%{valid_time: DateTime.add(now, 12 * 3600, :second)}) ==
"through +12h"
end
test "returns nil when no forecast hour is provided" do
test "valid_time in the past renders as 'through Nh ago'" do
now = DateTime.utc_now()
assert PipelineStatus.running_detail(%{valid_time: DateTime.add(now, -2 * 3600, :second)}) ==
"through 2h ago"
end
test "returns nil when no valid_time is provided" do
assert PipelineStatus.running_detail(nil) == nil
assert PipelineStatus.running_detail(%{}) == nil
assert PipelineStatus.running_detail(%{forecast_hour: 3}) == nil
end
end
end

View file

@ -218,23 +218,25 @@ defmodule MicrowavepropWeb.MapLiveTest do
assert html =~ "Updating propagation"
assert html =~ ~s(data-pipeline-state="running")
Phoenix.PubSub.broadcast(
Microwaveprop.PubSub,
"propagation:pipeline",
{:propagation_pipeline_progress, %{forecast_hour: 3, valid_time: ~U[2026-04-14 18:00:00Z]}}
)
html = render(lv)
assert html =~ "+3h"
future = DateTime.add(DateTime.utc_now(), 3 * 3600, :second)
Phoenix.PubSub.broadcast(
Microwaveprop.PubSub,
"propagation:pipeline",
{:propagation_pipeline_progress, %{forecast_hour: 0, valid_time: ~U[2026-04-14 15:00:00Z]}}
{:propagation_pipeline_progress, %{forecast_hour: 5, valid_time: future}}
)
html = render(lv)
assert html =~ "now"
assert html =~ "through +3h"
Phoenix.PubSub.broadcast(
Microwaveprop.PubSub,
"propagation:pipeline",
{:propagation_pipeline_progress, %{forecast_hour: 2, valid_time: DateTime.utc_now()}}
)
html = render(lv)
assert html =~ "through now"
end
end
end