Keep radar history per location from the IEM archive, with lead-in and self-healing gaps (#210) #211

Merged
claude-bot merged 1 commit from feat/location-radar-archive into main 2026-10-13 21:55:06 +00:00
Collaborator

Implements #210. Design decisions (tiers, 60/30 minute lead-in and tail, IEM as the source, shipping in v2.0.0) were made by @rbrooks on 2026-10-13.

What changes

Radar capture for the Explorer's event loops moves from "fetch NOAA's current image per alert, capped at 24" to "reconcile each location's frames against Iowa State's time-enabled archive".

  • location_radar_frames (migration 0038): one row per (location_id, valid_at) on a 5-minute grid. Overlapping alerts share frames.
  • app/services/radar_archive.py: tiering, slot maths, the IEM fetcher, blank detection, the fill job, coverage accounting and the incomplete-capture admin alert.
  • Fill job replaces poll_alert_radar_frames. Each run plans the slots every location's alerts want (alert life, plus 60 minutes before and 30 after), subtracts what is stored, and fetches the rest sequentially, oldest first, up to 120 per run. Alerts that ended in the last 48 hours are still planned, which is what repairs gaps.
  • Explorer loop reads archive frames when the event's own alerts schedule any, else falls back to the legacy per-alert frames. Never both. Loops over 120 frames are thinned evenly.
  • Caption reports stored against scheduled frames ("23 of 25 frames"), with a note when some are missing.
  • Retention covers the new table and its files on the existing radar-frame horizon.

Tiers

Tier Events Capture
Full Tornado, Severe Thunderstorm, Flash Flood, Snow Squall, Extreme Wind, Dust Storm, Special Marine Warnings; Tornado and Severe Thunderstorm Watches Every 5 min, whole alert (capped at 24 h)
Reduced Winter Storm, Blizzard, Ice Storm, Lake Effect Snow, Hurricane, Tropical Storm, Storm Surge, Flood, High Wind Warnings; Flood and Winter Storm Watches Every 15 min, first 24 h
None Everything else Nothing

Things worth a reviewer's attention

  • IEM returns HTTP 200 and a fully transparent PNG for a slot that does not exist yet. Slots younger than 10 minutes are never requested; a blank frame younger than 30 minutes is not stored and is retried. Older than that it is accepted as clear sky. A genuine hole in IEM's own archive would therefore be stored as a blank frame.
  • Never-clearing alerts. A dashboard row with no expiry that never clears reads as active indefinitely. Full-tier windows are capped at 24 hours, and an alert whose capped window is older than the lookback is skipped, so such a row cannot drive an endless backfill.
  • No paging during a backlog. The incomplete-capture alert only runs when the job attempted everything that was missing, so the first run after this deploys does not alert for every recent alert.
  • A heat warning gets no radar loop even if a storm left frames in its window: the archive path is used only when the event's own alerts schedule frames.
  • Storage keys are flat (location_radar/{location_id}_{YYYYMMDD}_{HHMM}.png): the media storage abstraction allows one directory level.
  • The #208 cadence fix is superseded: radar_frames.py is deleted along with the job it fixed. Cadence is now the 5-minute slot grid.

Unchanged

  • The alert-time snapshot (#85) for notifications and public pages still comes from NOAA. Nothing in the dispatch path calls IEM.
  • Existing alert_radar_frames rows, files and retention.
  • ALERT_RADAR_FRAME_MAX_PER_EVENT is still accepted in env files but no longer used.

How it was built

Implementation was delegated from a written spec; I reviewed the result and changed three things: the never-clearing-alert bounds, the backlog guard on the admin alert, and deriving an event's expected frame count from its own alerts' schedule (the first version inferred it from frame timestamps).

Verification

  • ruff: clean
  • Bulk tier: 1043 passed (41 new in tests/test_radar_archive.py)
  • The real fetcher was run against IEM for Wichita: two past slots returned 87 KB and 89 KB frames (not blank); a slot 10 minutes in the future returned 1,476 bytes and was detected as blank.
  • Not run locally: the Postgres tier and alembic upgrade head for 0038. CI runs both.
  • Not verified at all yet: the job running on a real deployment, an assembled loop from archive frames, and how the IEM colour table looks over the basemap. Those need dev.

After merge

  • The production migration rehearsal needs repeating, now 0033 → 0038.
  • On first run, dev backfills the last 48 hours for its qualifying alerts, including today's Kansas watch, in a few runs.

🤖 Generated with Claude Code

Implements #210. Design decisions (tiers, 60/30 minute lead-in and tail, IEM as the source, shipping in v2.0.0) were made by @rbrooks on 2026-10-13. ## What changes Radar capture for the Explorer's event loops moves from "fetch NOAA's current image per alert, capped at 24" to "reconcile each location's frames against Iowa State's time-enabled archive". - **`location_radar_frames`** (migration `0038`): one row per `(location_id, valid_at)` on a 5-minute grid. Overlapping alerts share frames. - **`app/services/radar_archive.py`**: tiering, slot maths, the IEM fetcher, blank detection, the fill job, coverage accounting and the incomplete-capture admin alert. - **Fill job** replaces `poll_alert_radar_frames`. Each run plans the slots every location's alerts want (alert life, plus 60 minutes before and 30 after), subtracts what is stored, and fetches the rest sequentially, oldest first, up to 120 per run. Alerts that ended in the last 48 hours are still planned, which is what repairs gaps. - **Explorer loop** reads archive frames when the event's own alerts schedule any, else falls back to the legacy per-alert frames. Never both. Loops over 120 frames are thinned evenly. - **Caption** reports stored against scheduled frames ("23 of 25 frames"), with a note when some are missing. - **Retention** covers the new table and its files on the existing radar-frame horizon. ## Tiers | Tier | Events | Capture | |---|---|---| | Full | Tornado, Severe Thunderstorm, Flash Flood, Snow Squall, Extreme Wind, Dust Storm, Special Marine Warnings; Tornado and Severe Thunderstorm Watches | Every 5 min, whole alert (capped at 24 h) | | Reduced | Winter Storm, Blizzard, Ice Storm, Lake Effect Snow, Hurricane, Tropical Storm, Storm Surge, Flood, High Wind Warnings; Flood and Winter Storm Watches | Every 15 min, first 24 h | | None | Everything else | Nothing | ## Things worth a reviewer's attention - **IEM returns HTTP 200 and a fully transparent PNG for a slot that does not exist yet.** Slots younger than 10 minutes are never requested; a blank frame younger than 30 minutes is not stored and is retried. Older than that it is accepted as clear sky. A genuine hole in IEM's own archive would therefore be stored as a blank frame. - **Never-clearing alerts.** A dashboard row with no expiry that never clears reads as active indefinitely. Full-tier windows are capped at 24 hours, and an alert whose capped window is older than the lookback is skipped, so such a row cannot drive an endless backfill. - **No paging during a backlog.** The incomplete-capture alert only runs when the job attempted everything that was missing, so the first run after this deploys does not alert for every recent alert. - **A heat warning gets no radar loop** even if a storm left frames in its window: the archive path is used only when the event's own alerts schedule frames. - **Storage keys are flat** (`location_radar/{location_id}_{YYYYMMDD}_{HHMM}.png`): the media storage abstraction allows one directory level. - **The #208 cadence fix is superseded**: `radar_frames.py` is deleted along with the job it fixed. Cadence is now the 5-minute slot grid. ## Unchanged - The alert-time snapshot (#85) for notifications and public pages still comes from NOAA. Nothing in the dispatch path calls IEM. - Existing `alert_radar_frames` rows, files and retention. - `ALERT_RADAR_FRAME_MAX_PER_EVENT` is still accepted in env files but no longer used. ## How it was built Implementation was delegated from a written spec; I reviewed the result and changed three things: the never-clearing-alert bounds, the backlog guard on the admin alert, and deriving an event's expected frame count from its own alerts' schedule (the first version inferred it from frame timestamps). ## Verification - ruff: clean - Bulk tier: **1043 passed** (41 new in `tests/test_radar_archive.py`) - The real fetcher was run against IEM for Wichita: two past slots returned 87 KB and 89 KB frames (not blank); a slot 10 minutes in the future returned 1,476 bytes and was detected as blank. - **Not run locally:** the Postgres tier and `alembic upgrade head` for `0038`. CI runs both. - **Not verified at all yet:** the job running on a real deployment, an assembled loop from archive frames, and how the IEM colour table looks over the basemap. Those need dev. ## After merge - The production migration rehearsal needs repeating, now `0033 → 0038`. - On first run, dev backfills the last 48 hours for its qualifying alerts, including today's Kansas watch, in a few runs. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Keep radar history per location from the IEM archive, with lead-in and self-healing gaps (#210)
All checks were successful
CI / test (pull_request) Successful in 6m22s
851b4fe5a0
Radar frames for the Explorer's event loops were captured per alert, live,
from NOAA, with a 24-frame cap. NOAA serves "now" only, so a frame not
fetched at the time was gone for good; a warning's loop only had "before"
frames if a watch happened to be active first; and a watch and a warning on
one location each fetched and stored the same picture.

Iowa State's archive serves the national NEXRAD reflectivity mosaic at
5-minute steps for arbitrary past times, with the same bbox request shape.
That turns capture into reconciliation:

- Frames belong to a location and a radar time (new table
  location_radar_frames, migration 0038), so overlapping alerts share them.
- The fill job computes the slots each location's alerts want (alert life
  plus 60 minutes before and 30 after) and fetches whichever are missing.
  It reconsiders alerts from the last 48 hours on every run, so a restart,
  an outage or a failed fetch is repaired on a later run.
- Lead-in no longer depends on a watch: it is fetched retroactively.
- Tiers by NWS event: short-fused radar-driven warnings and convective
  watches every 5 minutes for their whole life; long-duration precipitation
  alerts every 15 minutes for their first 24 hours; everything else nothing.
  Radar was previously captured for heat warnings.

IEM answers a slot that does not exist yet with HTTP 200 and a fully
transparent PNG, so a missing frame looks like clear sky. Slots younger than
10 minutes are never requested, and a blank frame younger than 30 minutes is
treated as not yet published and retried.

Bounds: requests are sequential and capped per run, oldest first. A
Full-tier alert's window is capped at 24 hours and an alert whose capped
window predates the lookback is ignored, because a dashboard row that never
clears and has no expiry would otherwise be backfilled without end.

The Explorer loop uses archive frames when the event's own alerts schedule
any and falls back to the legacy per-alert frames otherwise, never mixing the
two (different colour tables). Its caption reports stored against scheduled
frames. An admin alert fires for a Full-tier alert that ended 30 minutes ago
with under 90% of its frames, but not while a backlog is still being worked
through, so an upgrade does not page for every recent alert at once.

The alert-time snapshot used by notifications and public pages stays on
NOAA, and nothing in the alert dispatch path calls IEM. The per-alert
periodic capture job is removed; existing alert_radar_frames rows and files
stay readable under their own retention.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
rbrooks/WeatherBot!211
No description provided.