Explorer: derived radar loop windows for event replay (#140) #173

Merged
claude-bot merged 2 commits from feat/event-radar-loops into main 2026-08-01 05:44:54 +00:00
Contributor

Closes #140. Stacked on the event timeline (#139/PR #172, merged).

What this adds

GET /media/event-radar-loop/{event_id}.gif — a lazily-built, cached radar loop spanning an event's member window plus configurable padding (EVENT_RADAR_LOOP_PADDING_MINUTES, default 15).

  • Reuses the existing GIF machinery: radar.py's SPC assembly was extracted into a shared assemble_gif() (additive refactor); the event/DB-specific logic lives in a new event_radar_loop.py service, keeping radar.py DB-free as designed.
  • Lazy + cached through the media storage abstraction (#131), per the issue's own recommendation. Staleness is one mtime comparison: rebuild iff a frame newer than the cached artifact exists.
  • Honest partial coverage: the event page captions the player with actual coverage — "Radar loop · 14 frames · 6:05–7:40 PM (event 5:50–8:00 PM)" — and shows an explicit "no frames captured" state, so pruned/partial capture never masquerades as a full window. The page render uses a cheap DB-only coverage query; assembly cost is paid only when the GIF itself is requested.
  • Auth deviation from the rest of media.py, deliberately: every other media route is unauthenticated (also served on public pages); the loop is Explorer-only, so it requires login + the event's location visibility, with the same non-leaking 404 as the event page. Cache-Control: no-store since an active event's loop can gain frames.
  • 500 ms/frame (vs SPC's 5 s) — a radar loop reads as motion, not discrete category maps; tunable constant.

Review fix

One fixture bug: two byte-identical test frames assembled into a 1-frame GIF — Pillow's GIF writer merges identical consecutive frames (delta optimization). Real radar frames are never pixel-identical; fixtures now default to distinct colors per frame.

Verification

Full bulk suite on the dev server: 966 passed (12 new: window padding, endpoint 404s, real-GIF frame counts, cache hit (assembler call-count), staleness rebuild on newer frame, coverage caption, zero-frame state). ruff clean. No migrations, no new JS.

🤖 Generated with Claude Code

Closes #140. Stacked on the event timeline (#139/PR #172, merged). ## What this adds `GET /media/event-radar-loop/{event_id}.gif` — a lazily-built, cached radar loop spanning an event's member window plus configurable padding (`EVENT_RADAR_LOOP_PADDING_MINUTES`, default 15). - **Reuses the existing GIF machinery**: `radar.py`'s SPC assembly was extracted into a shared `assemble_gif()` (additive refactor); the event/DB-specific logic lives in a new `event_radar_loop.py` service, keeping `radar.py` DB-free as designed. - **Lazy + cached** through the media storage abstraction (#131), per the issue's own recommendation. Staleness is one mtime comparison: rebuild iff a frame newer than the cached artifact exists. - **Honest partial coverage**: the event page captions the player with actual coverage — "Radar loop · 14 frames · 6:05–7:40 PM (event 5:50–8:00 PM)" — and shows an explicit "no frames captured" state, so pruned/partial capture never masquerades as a full window. The page render uses a cheap DB-only coverage query; assembly cost is paid only when the GIF itself is requested. - **Auth deviation from the rest of `media.py`, deliberately**: every other media route is unauthenticated (also served on public pages); the loop is Explorer-only, so it requires login + the event's location visibility, with the same non-leaking 404 as the event page. `Cache-Control: no-store` since an active event's loop can gain frames. - 500 ms/frame (vs SPC's 5 s) — a radar loop reads as motion, not discrete category maps; tunable constant. ## Review fix One fixture bug: two byte-identical test frames assembled into a 1-frame GIF — Pillow's GIF writer merges identical consecutive frames (delta optimization). Real radar frames are never pixel-identical; fixtures now default to distinct colors per frame. ## Verification Full bulk suite on the dev server: **966 passed** (12 new: window padding, endpoint 404s, real-GIF frame counts, cache hit (assembler call-count), staleness rebuild on newer frame, coverage caption, zero-frame state). `ruff` clean. No migrations, no new JS. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Assembles an animated GIF from the periodic AlertRadarFrame series (#130)
captured across an event's [started_at - pad, ended_at + pad] window
(EVENT_RADAR_LOOP_PADDING_MINUTES, default 15 min). Lazy + cached: the
first GET assembles and stores the GIF through the media storage
abstraction (#131) under event_loops/{event_id}.gif; later requests serve
the cached artifact unless a newer frame has appeared, in which case it
rebuilds (cheap mtime-vs-frame-timestamp check).

- app/services/radar.py: extract assemble_gif() from fetch_spc_outlook_gif's
  inline Pillow logic so the event loop reuses the same GIF assembler.
- app/services/event_radar_loop.py: window derivation, bounded frame
  collection, lazy build/cache, and a cheap coverage-only path for the
  Explorer page (no storage/Pillow touch on a plain page render).
- app/api/media.py: GET /media/event-radar-loop/{event_id}.gif — the only
  auth-gated route in this module (can_view_location_with_db), since a
  loop is only ever reached from the authenticated Explorer, unlike the
  alert-radar stills also linked from public alert pages.
- app/services/explorer.py + app/templates/explorer_event.html: event_detail
  gains has_loop/loop_caption metadata so the honest partial-coverage
  caption ("14 frames · 6:05-7:40 PM (event 5:50-8:00 PM)") always
  accompanies the player instead of implying full-window coverage.
- app/config.py: EVENT_RADAR_LOOP_PADDING_MINUTES setting (default 15).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Give fixture radar frames distinct pixels
All checks were successful
CI / test (pull_request) Successful in 3m45s
9270eaaa07
Pillow's GIF writer merges identical consecutive frames (delta
optimization), so two byte-identical fixture frames assembled into a
1-frame GIF and broke the n_frames assertions. Real radar frames are
never pixel-identical; the fixtures now default each frame to a
distinct color.

Co-Authored-By: Claude Fable 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!173
No description provided.