fix(contact-detail): re-enqueue HRRR on every page view when profile missing

The 'Fetching HRRR atmospheric data' spinner stayed up forever for
contacts stuck in :queued without a matching hrrr_fetch_tasks row.
Ways a contact lands in that state:

  - HrrrPointEnqueuer.enqueue/1 raises, the rescue swallows the error
    and returns {:ok, 0}, but the caller still marked the contact
    :queued before it noticed the failure.
  - Rust worker marks a (valid_time) task :done with a points array
    that never included this contact's point, so no profile is ever
    written for it.
  - hrrr_fetch_tasks row is cleaned up by retention before the contact
    page loads.

Drop the hrrr_status != :queued short-circuit and always call the
enqueuer when the profile is nil. The ON CONFLICT clause in
HrrrPointEnqueuer.enqueue/1 unions points into the existing row,
resets :done/:failed rows back to :queued with attempt=0, and no-ops
when the point is already present — so re-entering on every page view
is cheap and self-healing.
This commit is contained in:
Graham McIntire 2026-04-20 16:37:13 -05:00
parent 00101ca928
commit ee732d03b1
No known key found for this signature in database
GPG key ID: F4ABF488E6029E59

View file

@ -748,14 +748,27 @@ defmodule MicrowavepropWeb.ContactLive.Show do
lat = contact.pos1 && contact.pos1["lat"]
lon = contact.pos1 && contact.pos1["lon"]
if lat && lon && contact.hrrr_status != :queued do
# Always re-enqueue when there's no profile on this page render.
# The `hrrr_status == :queued` guard used to short-circuit here, but
# contacts can end up stuck in :queued without a live task row
# (HrrrPointEnqueuer swallowing an exception, Rust worker marking a
# task :done with a points array that never contained this contact's
# point, or a later retention prune removing the row). Re-entering
# the upsert is idempotent — the ON CONFLICT clause in
# HrrrPointEnqueuer unions points, resets :done/:failed rows back
# to :queued, and no-ops if the point + valid_time already match.
if lat && lon do
valid_time = HrrrClient.nearest_hrrr_hour(contact.qso_timestamp)
{rlat, rlon} = Weather.round_to_hrrr_grid(lat, lon)
# Phase 3 Stream C: route retries through hrrr_fetch_tasks so the
# Rust hrrr-point-worker picks them up alongside backfill work.
{:ok, _} = HrrrPointEnqueuer.enqueue(%{valid_time => [{rlat, rlon}]})
Radio.set_enrichment_status!([contact.id], :hrrr_status, :queued)
if contact.hrrr_status != :queued do
Radio.set_enrichment_status!([contact.id], :hrrr_status, :queued)
end
{nil, %{contact | hrrr_status: :queued}}
else
{nil, contact}