diff --git a/lib/microwaveprop/propagation/pipeline_status.ex b/lib/microwaveprop/propagation/pipeline_status.ex index 7fe18da5..d554c8fa 100644 --- a/lib/microwaveprop/propagation/pipeline_status.ex +++ b/lib/microwaveprop/propagation/pipeline_status.ex @@ -30,10 +30,14 @@ defmodule Microwaveprop.Propagation.PipelineStatus do @type state :: :running | :idle | :stale | :unknown + @type worker_tag :: :grid | :asos + + @type running_detail :: %{worker: worker_tag(), label: String.t()} + @type t :: %{ state: state(), label: String.t(), - detail: String.t() | nil, + details: [running_detail()], last_update_at: DateTime.t() | nil } @@ -52,30 +56,41 @@ defmodule Microwaveprop.Propagation.PipelineStatus do @spec current() :: t() def current do - case running_worker() do - worker when is_binary(worker) -> + case running_workers() do + [] -> + build_idle_or_stale(latest_completed_at()) + + details -> %{ state: :running, label: "Updating propagation", - detail: base_running_detail(worker), + details: details, last_update_at: latest_completed_at() } - - nil -> - build_idle_or_stale(latest_completed_at()) end end - defp running_worker do - Repo.one( - from j in "oban_jobs", - where: j.state == "executing" and j.worker in ^@workers, - order_by: [desc: j.attempted_at], - limit: 1, - select: j.worker + # Returns every pipeline worker currently in executing state, in a + # stable grid-before-asos order so the chip sub-lines don't flicker + # as jobs come and go. + defp running_workers do + from(j in "oban_jobs", + where: j.state == "executing" and j.worker in ^@workers, + distinct: j.worker, + select: j.worker ) + |> Repo.all() + |> Enum.sort_by(&worker_sort_key/1) + |> Enum.map(&to_running_detail/1) end + defp to_running_detail(@grid_worker), do: %{worker: :grid, label: "HRRR run"} + defp to_running_detail(@asos_worker), do: %{worker: :asos, label: "ASOS nudge"} + + defp worker_sort_key(@grid_worker), do: 0 + defp worker_sort_key(@asos_worker), do: 1 + defp worker_sort_key(_), do: 99 + defp latest_completed_at do case Repo.one( from j in "oban_jobs", @@ -91,7 +106,7 @@ defmodule Microwaveprop.Propagation.PipelineStatus do end defp build_idle_or_stale(nil) do - %{state: :unknown, label: "Propagation status unknown", detail: nil, last_update_at: nil} + %{state: :unknown, label: "Propagation status unknown", details: [], last_update_at: nil} end defp build_idle_or_stale(%DateTime{} = last) do @@ -101,23 +116,19 @@ defmodule Microwaveprop.Propagation.PipelineStatus do %{ state: :stale, label: "Propagation data stale · last update #{format_age(age_minutes)} ago", - detail: nil, + details: [], last_update_at: last } else %{ state: :idle, label: "Up to date · #{format_age(age_minutes)} ago", - detail: nil, + details: [], last_update_at: last } end end - defp base_running_detail(@grid_worker), do: "HRRR run" - defp base_running_detail(@asos_worker), do: "ASOS nudge" - defp base_running_detail(_), do: nil - defp format_age(0), do: "just now" defp format_age(1), do: "1m" defp format_age(n) when n < 60, do: "#{n}m" diff --git a/lib/microwaveprop_web/live/map_live.ex b/lib/microwaveprop_web/live/map_live.ex index 6adb6786..9a9fc683 100644 --- a/lib/microwaveprop_web/live/map_live.ex +++ b/lib/microwaveprop_web/live/map_live.ex @@ -412,13 +412,13 @@ defmodule MicrowavepropWeb.MapLive do attr :progress, :any, default: nil defp pipeline_status_chip(assigns) do - {main, detail} = chip_display_parts(assigns.status, assigns.progress) + details = chip_detail_labels(assigns.status, assigns.progress) assigns = assigns - |> assign(:display_main, main) - |> assign(:display_detail, detail) - |> assign(:display_title, if(detail, do: "#{main} — #{detail}", else: main)) + |> assign(:display_main, assigns.status.label) + |> assign(:display_details, details) + |> assign(:display_title, chip_title(assigns.status.label, details)) ~H"""
{@display_main} - <%= if @display_detail do %> - {@display_detail} + <%= for d <- @display_details do %> + {d} <% end %> <% :idle -> %> @@ -453,11 +453,23 @@ defmodule MicrowavepropWeb.MapLive do """ end - defp chip_display_parts(%{state: :running} = status, progress) do - {status.label, PipelineStatus.running_detail(progress) || status.detail} + # Map the PipelineStatus details list to a plain list of sub-line + # strings. For the grid worker specifically, the forecast-hour + # progress payload overrides the base label so the chip can show + # "+3h" / "now" instead of the generic "HRRR run". + defp chip_detail_labels(%{state: :running, details: details}, progress) do + override = PipelineStatus.running_detail(progress) + + Enum.map(details, fn + %{worker: :grid} -> override || "HRRR run" + %{label: label} -> label + end) end - defp chip_display_parts(status, _progress), do: {status.label, nil} + defp chip_detail_labels(_status, _progress), do: [] + + defp chip_title(main, []), do: main + defp chip_title(main, details), do: "#{main} — #{Enum.join(details, " · ")}" @impl true def render(assigns) do diff --git a/test/microwaveprop/propagation/pipeline_status_test.exs b/test/microwaveprop/propagation/pipeline_status_test.exs index 7a0ee22a..4b43f917 100644 --- a/test/microwaveprop/propagation/pipeline_status_test.exs +++ b/test/microwaveprop/propagation/pipeline_status_test.exs @@ -41,7 +41,7 @@ defmodule Microwaveprop.Propagation.PipelineStatusTest do assert is_binary(status.label) end - test "returns :running with grid worker detail when PropagationGridWorker is executing" do + test "returns :running with a single grid-worker detail when only PropagationGridWorker is executing" do insert_oban_job(%{ state: "executing", worker: @grid_worker, @@ -52,10 +52,11 @@ defmodule Microwaveprop.Propagation.PipelineStatusTest do assert status.state == :running assert status.label == "Updating propagation" - assert status.detail =~ "HRRR" + assert [%{worker: :grid, label: grid_label}] = status.details + assert grid_label =~ "HRRR" end - test "returns :running with ASOS detail when AsosAdjustmentWorker is executing" do + test "returns :running with a single ASOS detail when only AsosAdjustmentWorker is executing" do insert_oban_job(%{ state: "executing", worker: @asos_worker, @@ -66,7 +67,36 @@ defmodule Microwaveprop.Propagation.PipelineStatusTest do assert status.state == :running assert status.label == "Updating propagation" - assert status.detail =~ "ASOS" + assert [%{worker: :asos, label: asos_label}] = status.details + assert asos_label =~ "ASOS" + end + + test "returns :running with BOTH workers when grid + ASOS are executing at the same time" do + # Queue concurrency was bumped to 2 so the chain-step grid worker + # and the 10-minute ASOS nudge can run in parallel. The chip + # should show one sub-line per running worker, in a deterministic + # grid-before-asos order so the eye doesn't see them flicker. + insert_oban_job(%{ + state: "executing", + worker: @grid_worker, + attempted_at: minutes_ago(5) + }) + + insert_oban_job(%{ + state: "executing", + worker: @asos_worker, + attempted_at: minutes_ago(1) + }) + + status = PipelineStatus.current() + + assert status.state == :running + assert status.label == "Updating propagation" + assert [grid, asos] = status.details + assert grid.worker == :grid + assert grid.label =~ "HRRR" + assert asos.worker == :asos + assert asos.label =~ "ASOS" end test "returns :idle with Up to date label when last grid run completed recently" do