Capture radar snapshots for every alert, with an independent retention window #130
Labels
No labels
area:ai
area:ci-cd
area:notifications
area:observability
area:public-pages
backlog
bug
duplicate
enhancement
help wanted
invalid
question
type:decision
type:feature
type:infra
type:maintenance
type:security
v1.0.1
v1.1.0
v1.2.0
v1.3.0
v2.0.0
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
rbrooks/WeatherBot#130
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Parent: #22.
ALERT_RADAR_SNAPSHOT_ENABLEDdefaults to False (app/config.py:180), so the radar-at-alert-time images built in #85 — explicitly to front-load Explorer history — are not captured on a default install.Two problems beyond the default:
_cleanup_alert_radar_snapshots(app/services/retention.py:116-145) only deletes orphaned PNGs — files whose name no longer matches a livesent_alerts.radar_snapshot_path. Image lifetime is therefore whateversent_alertshappens to be, and sincesent_alertsprunes oncleared_at, a row that never gets cleared keeps its image forever._radar_snapshot_filename(alert_processor.py:724-733) is deterministic ({lifecycle_id}__{location_id}.png), so a re-fire overwrites the previous capture. Fine for "conditions when it first fired"; insufficient for a loop.Tasks
ALERT_RADAR_SNAPSHOT_ENABLEDto true.sent_alerts.cleared_at.Picking this up on branch
feat/radar-frames.Cadence decided: periodic frames with a per-event cap. Rationale is the same one that made #129 urgent — uncaptured history is unrecoverable, and #140 (radar loops) is far enough down the queue that shipping single-frame now would leave every event before it unreconstructable.
Rough sizing that informed the call, at ~120 KB/PNG and ~150 alerts/location/year:
~10x, judged affordable on disk. The cap is the important guardrail: it bounds both disk and outbound load on NOAA's MapServer during an outbreak, when many alerts are active at once.
Implementation shape
alert_radar_framestable (FK to the dashboardsent_alertsrow,captured_at,path), plus migration0031._radar_snapshot_filenamecurrently returns a deterministic name per(lifecycle, location), so re-fires overwrite. Frames need a timestamped filename so they accumulate.SentAlert.radar_snapshot_path/radar_snapshot_atstay as the representative first frame, so/media/alert-radar/{id}.pngand the public alert page keep working unchanged. Frames are purely additive.ALERT_RADAR_FRAME_INTERVAL_MINUTES,ALERT_RADAR_FRAME_MAX_PER_EVENT,RETENTION_ALERT_RADAR_FRAMES_DAYS;ALERT_RADAR_SNAPSHOT_ENABLEDflips to true.Deferred to #131: routing writes through the media storage abstraction. That issue moves every media call site at once, and this one will be among them — doing it here would mean writing the same code twice.
Done in #144 (merged to
main).Shipped:
alert_radar_framestable (migration0031), apoll_alert_radar_framesscheduler job appending frames to each active dashboard alert on a 5-minute interval with a 24-frame per-event cap,ALERT_RADAR_SNAPSHOT_ENABLEDflipped to true, an owned retention window for frames, andGET /media/alert-radar/frames/{frame_id}.png.SentAlert.radar_snapshot_path/_atare unchanged as the representative first frame, so the existing media route and public alert page are unaffected.Worth knowing for #140: the first frame deliberately shares its filename with
radar_snapshot_path— one file on disk, not two. That plus independent retention windows meant each retention sweep could delete a file the other still owned, in both directions. Both are now guarded and regression-tested. Anything that later moves these files (#131's storage abstraction, in particular) must preserve that mutual protection.Verified locally in Docker against every CI gate: ruff clean,
compileallOK, 791 SQLite-tier tests passed,alembic upgrade headclean through0031, a0031downgrade → re-upgrade round trip clean on Postgres 16, 4 Postgres-tier tests passed. CI on the PR was green in 3m8s.Open follow-ups:
alert_radar_framesrow count and the on-disk size of{radar_cache_dir}/alert_snapshotsafter a real storm season, and feeding the number into #133 before the unified horizon is fixed.Measured — the ~165 MB/location/year estimate is high by roughly 1.5–2.3×
Full measurement set is on #133 (comment); the radar-frame part, since this issue asked for it:
Measured in prod at 2026-07-28 14:30 UTC:
radar_cache/alert_snapshotsalert_radar_frames, n=2 eventsalert_radar_frame_max_per_event = 24ceiling: ≈ 113 MB/location/yearBoth land below the 165 MB/location/year estimated here, so the original figure was conservative in the right direction — but it is worth having the real number before #133 locks a horizon that multiplies it by 13 months.
Caveat, stated plainly: the frames-per-event average rests on n=2 events, because capture only ungated at 03:50 UTC today and the DB holds 31 frames total. The event rate is solid (839 records over 92 days); frames-per-event is not. Worth re-measuring after a real convective season before treating 73 MB as settled.
Also measured:
alert_radar_framescosts 96 kB of database for 31 rows, and that is 80 kB index against 16 kB heap — at this row count it is all fixed index-page overhead. The PNG bytes on disk dominate the DB rows by orders of magnitude, so disk is the number that matters for this feature, not table size.