feat(rover-planning): backfill resets stale :complete paths to repopulate term blob

Existing Path rows persisted before the PathCompute extraction shipped
have no "term" key in their result map, so the live /path page hits
the :stale_term branch and falls back to live recompute when the
operator clicks a row. The hourly backfill cron now flips those
:complete rows back to :pending so the path-profile worker rerolls
them with the new full-output term, after which row clicks render
from cache instantly. The flip uses Postgres's jsonb_exists() so it's
a single targeted UPDATE instead of pulling rows into Elixir.
This commit is contained in:
Graham McIntire 2026-05-03 15:16:42 -05:00
parent c9aa9c53d5
commit 1abf452558
No known key found for this signature in database
GPG key ID: F4ABF488E6029E59

View file

@ -175,17 +175,20 @@ defmodule Microwaveprop.RoverPlanning do
end
@doc """
Walks every mission and re-enqueues any (rover_location × station)
pair whose `Path` is missing or not `:complete`. Existing complete
rows are left alone the worker also short-circuits on `:complete`,
so this is idempotent and safe to run on a cron.
Walks every mission and re-enqueues any (rover_location × station ×
band) pair whose `Path` is missing, not `:complete`, OR whose
cached `result` lacks the `"term"` blob (paths persisted before
PathCompute extraction shipped the row click on /rover-planning
needs that blob to render `/path` from cache instead of recomputing).
Use case: a rolling deploy or a transient elevation-API failure left
some missions with `:pending` / `:failed` entries; the next backfill
tick recovers them without operator action.
Idempotent: the path-profile worker short-circuits on `:complete`,
and we only nudge :complete rows back to :pending when their term
is missing. Safe to run on a cron.
"""
@spec backfill_paths() :: :ok
def backfill_paths do
reset_paths_missing_term()
Mission
|> Repo.all()
|> Repo.preload(stations: from(s in Station, order_by: s.position))
@ -194,6 +197,20 @@ defmodule Microwaveprop.RoverPlanning do
:ok
end
# Flip :complete paths whose result map has no "term" key back to
# :pending so the next reconcile pass picks them up. JSONB
# `key_exists` is `?` in pg, but Ecto's fragment uses `?` as a
# placeholder — use the function form `jsonb_exists/2` instead.
defp reset_paths_missing_term do
Repo.update_all(
from(p in Path,
where: p.status == :complete,
where: not fragment("jsonb_exists(?, 'term')", p.result)
),
set: [status: :pending]
)
end
defp enqueue_paths_for(%Mission{} = mission) do
mission
|> desired_tuples()