diff --git a/lib/microwaveprop_web/live/status_live.ex b/lib/microwaveprop_web/live/status_live.ex index 82eeaee6..d0d8e5b0 100644 --- a/lib/microwaveprop_web/live/status_live.ex +++ b/lib/microwaveprop_web/live/status_live.ex @@ -14,11 +14,17 @@ defmodule MicrowavepropWeb.StatusLive do if connected?(socket) do Phoenix.PubSub.subscribe(Microwaveprop.PubSub, "db:contact_status") Phoenix.PubSub.subscribe(Microwaveprop.PubSub, "db:oban_jobs") + # Rust prop-grid-rs emits NOTIFY propagation_ready on completion — + # PropagationNotifyListener fans it out as `propagation:updated` so + # the grid_tasks panel transitions from "running" → "done" without + # a manual refresh. + Phoenix.PubSub.subscribe(Microwaveprop.PubSub, "propagation:updated") end stats = fetch_stats() unprocessed = count_unprocessed() db_stats = fetch_db_stats() + grid_tasks = fetch_grid_tasks_stats() {:ok, assign(socket, @@ -26,6 +32,7 @@ defmodule MicrowavepropWeb.StatusLive do stats: stats, unprocessed: unprocessed, db_stats: db_stats, + grid_tasks: grid_tasks, refresh_timer: nil )} end @@ -39,12 +46,17 @@ defmodule MicrowavepropWeb.StatusLive do {:noreply, schedule_refresh(socket)} end + def handle_info({:propagation_updated, _valid_times}, socket) do + {:noreply, schedule_refresh(socket)} + end + def handle_info(:refresh_stats, socket) do {:noreply, assign(socket, stats: fetch_stats(), unprocessed: count_unprocessed(), db_stats: fetch_db_stats(), + grid_tasks: fetch_grid_tasks_stats(), refresh_timer: nil )} end @@ -165,6 +177,53 @@ defmodule MicrowavepropWeb.StatusLive do ) end + defp fetch_grid_tasks_stats do + Cache.fetch_or_store({__MODULE__, :grid_tasks_stats}, 2_000, fn -> fetch_grid_tasks_stats_raw() end) + end + + # Snapshot of the Rust prop-grid-rs work queue. Mirrors the Oban panel — + # "what's running, what's pending, what's retrying" — but pulls from the + # grid_tasks hand-off table instead of oban_jobs. + defp fetch_grid_tasks_stats_raw do + running = + Repo.all( + from(t in "grid_tasks", + where: t.status == "running", + order_by: [asc: t.run_time, asc: t.forecast_hour], + select: %{ + run_time: t.run_time, + forecast_hour: t.forecast_hour, + attempt: t.attempt, + claimed_at: t.claimed_at + } + ) + ) + + counts_by_status = + from(t in "grid_tasks", + where: t.status in ["queued", "failed"], + group_by: t.status, + select: {t.status, count(t.id)} + ) + |> Repo.all() + |> Map.new() + + done_1h = + Repo.one( + from(t in "grid_tasks", + where: t.status == "done" and t.completed_at > ago(1, "hour"), + select: count(t.id) + ) + ) + + %{ + running: running, + queued: Map.get(counts_by_status, "queued", 0), + failed: Map.get(counts_by_status, "failed", 0), + done_1h: done_1h + } + end + defp fetch_stats do Cache.fetch_or_store({__MODULE__, :stats}, 2_000, fn -> fetch_stats_raw() end) end @@ -456,6 +515,62 @@ defmodule MicrowavepropWeb.StatusLive do
+| Worker | +Running | +Queued | +Retrying | +Done (1h) | +
|---|---|---|---|---|
| prop-grid-rs | ++ <%= if @grid_tasks.running == [] do %> + 0 + <% else %> + + + + <%= for r <- @grid_tasks.running do %> + + f{String.pad_leading(Integer.to_string(r.forecast_hour), 2, "0")} + + <% end %> + + running + + <% end %> + | +{@grid_tasks.queued} | ++ <%= if @grid_tasks.failed > 0 do %> + {@grid_tasks.failed} + <% else %> + 0 + <% end %> + | +{format_number(@grid_tasks.done_1h)} | +