Late-delivered or redelivered events for passes with los_epoch < now
were broadcasting as if upcoming. Guard added after los_epoch parse,
before dedup/DB work.
Ongoing passes (aos past, los future) still broadcast. los_epoch=None
falls through unchanged.
Existing tests pinned to fixed now= values to avoid false staleness
rejections on hardcoded envelope timestamps.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
GUI saves norad_ids as JSON strings (["25544"]), wire delivers norad_id
as int. Membership test `25544 in ["25544"]` was False — opt-in list
silently matched nothing.
Build allow_set as {int(x) for x in norad_ids_raw if str(x).isdigit()},
accept both string and int shapes forever. Garbage entries silently
skipped. satpass_cmd already coerces via [int(x) for x in cfg_ids].
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Incident response for 343 broadcasts in 126s (2026-06-12 22:10 UTC).
Five safety controls:
1. OPT-IN BIRD FILTER: norad_ids=[] now means "broadcast nothing"
(was "all birds"). Empty list logs once at INFO and suppresses all
broadcasts. The !satpass DM command remains ungated — it queries
any bird in the TLE cache using command_norad_ids as bare-command
default. Two paths, two rules.
2. RATE CAP: new satpass.max_broadcasts_per_hour (int, default 4).
Excess qualifying passes logged and suppressed. Broadcast path only.
3. DRY-RUN MODE: new satpass.dry_run (bool, default TRUE). Logs exact
wire text at INFO prefixed "DRY-RUN would air:" without dispatching.
Go-live: enabled=true + dry_run=true → observe → dry_run=false.
4. ELEVATION DEFAULT: min_elevation REGISTRY default already at 30
(confirmed, no change needed).
5. BROADCAST WIRE FORMAT: two-line LoRa-tight format with buckets:
🛰️ {name} {bucket}, {aos_compass}→{los_compass}
{duration} minute window, {rise}–{set} {AM/PM} MDT
Buckets: overhead (≥60°), high pass (30-59°), low pass (<30°).
DM format keeps exact degrees. One format_pass() function with
broadcast= mode switch — two callers, one function.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>