The running label "Updating propagation (HRRR run)" didn't fit in the 224 px desktop sidebar alongside the NTMS title block, so it was being truncated. Move the chip to the bottom of the sidebar (below the auth menu) where it can use the full sidebar width, let the text wrap naturally instead of truncating, and drop the trailing ellipsis. Mobile chip stays at the top since the mobile layout collapses menu + auth into a dropdown.
113 lines
3.2 KiB
Elixir
113 lines
3.2 KiB
Elixir
defmodule Microwaveprop.Propagation.PipelineStatus do
|
||
@moduledoc """
|
||
Aggregated status for the propagation update pipeline.
|
||
|
||
The pipeline is driven by two Oban workers:
|
||
|
||
* `PropagationGridWorker` — hourly, fetches HRRR f00–f18, scores
|
||
the CONUS grid, and broadcasts `propagation:updated`.
|
||
* `AsosAdjustmentWorker` — every 10 minutes, nudges scores with
|
||
recent ASOS surface observations.
|
||
|
||
`current/0` returns one struct so the map page can render a single
|
||
status chip ("Updating…", "Up to date · 8m ago", "Stale · 4h ago")
|
||
that reflects the whole pipeline rather than tailing a single job.
|
||
"""
|
||
|
||
import Ecto.Query
|
||
|
||
alias Microwaveprop.Repo
|
||
|
||
@grid_worker "Microwaveprop.Workers.PropagationGridWorker"
|
||
@asos_worker "Microwaveprop.Workers.AsosAdjustmentWorker"
|
||
|
||
@workers [@grid_worker, @asos_worker]
|
||
|
||
# Scores older than this flip the chip to :stale. Matches the
|
||
# FreshnessMonitor threshold — anything past 2h is already a gap
|
||
# the pipeline should have caught.
|
||
@stale_after_minutes 120
|
||
|
||
@type state :: :running | :idle | :stale | :unknown
|
||
|
||
@type t :: %{
|
||
state: state(),
|
||
label: String.t(),
|
||
last_update_at: DateTime.t() | nil
|
||
}
|
||
|
||
@spec current() :: t()
|
||
def current do
|
||
case running_worker() do
|
||
worker when is_binary(worker) ->
|
||
%{
|
||
state: :running,
|
||
label: running_label(worker),
|
||
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
|
||
)
|
||
end
|
||
|
||
defp latest_completed_at do
|
||
case Repo.one(
|
||
from j in "oban_jobs",
|
||
where: j.state == "completed" and j.worker in ^@workers,
|
||
order_by: [desc: j.completed_at],
|
||
limit: 1,
|
||
select: j.completed_at
|
||
) do
|
||
nil -> nil
|
||
%NaiveDateTime{} = naive -> DateTime.from_naive!(naive, "Etc/UTC")
|
||
%DateTime{} = dt -> dt
|
||
end
|
||
end
|
||
|
||
defp build_idle_or_stale(nil) do
|
||
%{state: :unknown, label: "Propagation status unknown", last_update_at: nil}
|
||
end
|
||
|
||
defp build_idle_or_stale(%DateTime{} = last) do
|
||
age_minutes = max(DateTime.diff(DateTime.utc_now(), last, :minute), 0)
|
||
|
||
if age_minutes > @stale_after_minutes do
|
||
%{
|
||
state: :stale,
|
||
label: "Propagation data stale · last update #{format_age(age_minutes)} ago",
|
||
last_update_at: last
|
||
}
|
||
else
|
||
%{
|
||
state: :idle,
|
||
label: "Up to date · #{format_age(age_minutes)} ago",
|
||
last_update_at: last
|
||
}
|
||
end
|
||
end
|
||
|
||
defp running_label(@grid_worker), do: "Updating propagation (HRRR run)"
|
||
defp running_label(@asos_worker), do: "Updating propagation (ASOS nudge)"
|
||
defp running_label(_), do: "Updating propagation"
|
||
|
||
defp format_age(0), do: "just now"
|
||
defp format_age(1), do: "1m"
|
||
defp format_age(n) when n < 60, do: "#{n}m"
|
||
|
||
defp format_age(n) do
|
||
hours = div(n, 60)
|
||
mins = rem(n, 60)
|
||
if mins == 0, do: "#{hours}h", else: "#{hours}h #{mins}m"
|
||
end
|
||
end
|