If LAST_PER_SUBJECT has no pending messages (container was only down
briefly), no _on_message callback fires and drain mode never exits.
Add a call_later timeout that auto-completes drain after 30s of no
num_pending==0 trigger.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
nats-py's Msg.metadata is a property returning a Metadata object
directly — not an async callable. Removes the erroneous () call.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
After a NATS consumer outage, LAST_PER_SUBJECT delivery floods thousands
of events in seconds. Fire events with _severity_override="immediate"
bypassed the Grouper and zeroed dispatcher cooldowns, causing duplicate
"New" broadcasts for the same fire.
Three-part fix:
- Downgrade fire severity from "immediate" to "priority" so pipeline
guards (Grouper, cooldown) apply normally
- Add FirePacer (FIFO queue, <=1 fire broadcast/min) for rate-limiting
- Add drain mode to CentralConsumer: suppress bus.emit() during backlog
catch-up, then run a decision pass per fire IrwinID against final DB
state (New/Update/Closure/Silence) and route through pacer
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
satpass_predict envelopes carry only raw azimuth_at_aos/los/peak as
float degrees — they lack the precomputed _compass string fields that
n2yo envelopes provide. The handler read only the _compass fields,
producing empty compass directions ("→") in broadcast wire text.
Apply _azimuth_to_compass() as fallback when _compass strings are
absent, preserving the existing string-field preference for n2yo.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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>
Tracking panel gains both new adapter-config keys:
- dry_run toggle (sky-blue accent, visually distinct from enabled)
- max_broadcasts_per_hour number input (1–60 range)
Armed-state banner at top of panel:
- OFF (grey) when disabled
- DRY RUN (sky-blue) when enabled + dry_run
- ⚠ LIVE (amber, pulsing) when enabled + !dry_run
Load/save through adapter-config API per 199929f pattern.
NORAD IDs helper updated to reflect opt-in semantics.
Bundle hash: index-DPN58SF4.js → index-Di1mw816.js
index-CB06j1ej.css → index-WwNJt5S-.css
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>
satpass_handler.py: fix 6 field reads to match actual Central NATS
wire names (max_elevation_deg, satellite_name, observer_name/slug,
aos_time, los_time, azimuth_at_peak_compass).
consumer.py: add pass. prefix to _CATEGORY_MAP so
pass.n2yo_visualpasses routes to sat_pass instead of default-deny.
Add sat domain fallback to _SUBJECT_DOMAIN_CATEGORY.
Tests: 9 new tests using verbatim live NOAA-18 envelope from
central.sat.pass.us.id.filer (2026-06-10). Updates existing
test_satpass_handler.py fixtures to match corrected wire names.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The Tracking panel toggle for satpass.enabled was silently dropped on
save because SatpassConfig lacked the enabled field. The toggle wrote
to a phantom env.satpass.enabled (not in EnvConfig) which the
environmental config endpoint ignored. Handlers read enabled from the
adapter_config SQLite table, which was never updated.
Changes:
- Add enabled:boolean to SatpassConfig interface + initial state
- Convert GET /api/adapter-config/satpass array response to keyed dict
so load actually reads saved values (fixes pre-existing load bug)
- Wire enabled into the save path via saveAdapterConfig PUT
- Override AdapterPanel enabled/onEnabled for satpass to use
satpassConfig instead of the phantom env field
- Add test_bool_roundtrip.py: 6 tests proving PUT true/false round-
trips through DB (value_json) and accessor (Python bool), plus
second+third adapter boolean round-trips (wfigs, fires)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Central publishes sat events with wire adapter names celestrak_tle,
n2yo_visualpasses, and satpass_predict. Task C's routing matched on
sat_tles/sat_tle/sat_passes — names that exist nowhere on the wire —
so every sat event was silently dropped at three kill points:
1. CENTRAL_ADAPTER_TO_SOURCE map (consumer.py line ~216)
2. _normalize dispatch conditionals (consumer.py line ~549)
3. satpass_handler adapter guard (satpass_handler.py line ~118)
All three now use the correct wire names. Both handlers also gain an
enabled gate that returns None when satpass.enabled=false, logging at
INFO once per process lifetime via a _disabled_logged function flag.
Existing test fixtures updated for the new adapter names and the
enabled gate (cfg.enabled=True in mock, _disabled_logged cleanup).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
_subjects_for() consults a per-adapter region table when region is set
(e.g. us.id). The table had no satpass entry, so it returned [] and
Central silently subscribed to nothing for sat subjects.
Changes:
- Add satpass entry to the region table: pass alerts use Convention A
(central.sat.pass.{region}.>), TLEs are global (central.sat.tle.>)
- Update test_central_region_default_propagates_to_consumer_subjects
to expect satpass subjects (satpass defaults to feed_source=central)
- Add 3 tests in test_satpass_registration.py: _subjects_for with
region, without region, and empty region fallback
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
_subject_owned() iterates _SUBJECTS_BARE keys and calls
getattr(self._env, attr) — but EnvironmentalConfig had no satpass
attribute, so Central never subscribed to central.sat.> subjects.
Changes:
- Add SatpassConfig(_SourcedFeed) with feed_source=central default
- Add satpass field to EnvironmentalConfig
- Add satpass YAML parsing handler in _dict_to_dataclass
- Add (satpass, enabled) to adapter_config REGISTRY
- Fix 3 satpass REGISTRY entries: type list -> json (vocabulary bug)
- Update stale count assertions (SCHEMA 16->17, REGISTRY 84->90,
ADAPTER_META 21->23)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Four-part implementation:
- Part 1: TLE cache handler subscribes central.sat.tle.>, upserts latest-wins
on epoch, 14-day staleness exclusion at read time (tle_handler.py)
- Part 2: SGP4 pass predictor with 30s step propagation, ECI→topocentric
look angles, grouped into PassInfo objects (pass_predictor.py)
- Part 3: ZCTA centroid CSV vendored (33,144 ZIPs), lazy-loaded for
ZIP→lat/lon lookup with no runtime network dependency
- Part 4: !satpass command with three forms: bare (adapter_config defaults),
name/NORAD ID, ZIP code observer. Location chain: GPS→ZIP→ask.
Reply formatted as 'ISS 09:36-09:43 MDT max 64deg SW->NE'
Schema v16→v17 (sat_tles table). sgp4>=2.22 added to requirements.
24 new tests covering TLE upsert/staleness, pass prediction reference,
ZCTA lookup, command routing, location chain, and reply format.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Tombstone closure broadcasts were silently dropped because the handler
set severity to routine (below the dispatcher floor) and never called
_attach_commit_handles(), leaving event_log.handled=0 permanently.
Changes:
- wfigs_handler.py: tombstone severity routine → immediate
- wfigs_handler.py: attach commit handles with dedup suffix closed
- wfigs_handler.py: use _log_event_returning_id so commit callback
can flip handled=1 on successful broadcast
- env_reporter.py: build_env_summary fire count excludes tombstoned
and 100%-contained fires (matches build_fires_detail WHERE)
- tests/test_tombstone_broadcast.py: 11 tests covering severity,
commit handles, dedup suffix, callback, closure wire, and
env_summary exclusion
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Chat LLM context selected top-10 fires by acreage only; small fresh
incidents (IA/assist fires) ranked below the cutoff and were invisible
to chat. Also excluded: tombstoned fires were listed as active.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Environment: add satpass adapter to Tracking tab with observers,
min_elevation, and norad_ids controls via adapter_config API
- Notifications: add satpass family to master toggles
- Rebuild dashboard bundle
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Wrap each adapter registration in try/except via _register_adapter()
- Track failed adapters in _failed_adapters dict with error message
- Add get_status() method for /api/env/status to report failed adapters
- No single adapter failure can abort loading of remaining adapters
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Replace bg-slate-800/60 and bg-slate-800 with bg-bg-card/bg-bg
- Replace border-slate-700 with border-border
- Replace divide-slate-700/60 with divide-border
- Replace hover:bg-slate-800/50 and hover:bg-slate-700 with hover:bg-bg-hover
- Remove all rounded classes for sharp Carbon edges
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
text-[#333] → text-[#666] (63 hits), text-[#444] → text-[#777] (38 hits)
across all tsx/css. Scrollbar thumb #1e1e1e → #2a2a2a.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Global: removed all rounded-lg/md/sm classes, replaced blue-500 with
sky-400 informational, cyan accents with amber across all tsx files.
Environment.tsx + AdapterConfig.tsx: full Carbon color sweep — slate
hierarchy replaced with #333/#444/white tokens, border-border tokens,
font-sans labels, font-mono values. Layout.tsx logo text-[15px].
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Carbon palette: #111111/#0d0d0d backgrounds, #1e1e1e borders, traffic-light
data colors (green/red/sky/amber), 10px uppercase card headers, StatCard
colored border-tops, Layout sidebar amber bar + right nav indicator.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Tailwind config: semantic color tokens (amber/blue/danger), borderRadius all zero except full, Inter + JetBrains Mono font families
- index.css: Google Fonts imports, sharp scrollbar, scanline card texture, amber-glow utility
- Dashboard.tsx: three-color accent system (amber primary, blue informational, danger alerts), Inter for labels/prose, JetBrains Mono strictly for data values, all rounded corners removed, tighter spacing (p-4 cards, gap-4 grids, space-y-2 lists), underline-style tabs with amber active indicator, band condition dot indicators, source status dots remapped to semantic colors, StatCard supports per-instance value coloring, 36px minimum touch targets on interactive elements
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Active Alerts panel now has pill-style tab switcher (Active Alerts | Event Feed)
- LiveEventFeed supports embedded mode (no card wrapper) for tab use
- HepburnTropoCard moved from bottom row to middle row (replaces LiveEventFeed)
- Bottom row removed (empty after tropo card move)
- Fixed broken unicode escapes in BandConditionsCard (satellite, circle, moon emoji)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds HepburnTropoCard to the dashboard with a 23-region dropdown
selector. Selected region is persisted to adapter_config via
(dashboard, tropo_region) key, defaulting to wam (Western North
America). Image loads from dxinfocentre.com/tr_map/fcst/{code}006.png
with date-based cache busting.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The periodic health_update in main.py only sent score/tier/totals but
no pillars dict. Dashboard setHealth() replaced the full REST response
with this incomplete object, causing all pillar bars to drop to 0.0.
Now sends the complete payload matching mesh_routes.py and ws.py
(pillars, util_percent, flagged_nodes, battery_warnings, recommendations)
and gates on composite > 0 + infra_total > 0 to skip partial state.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Coverage pillar was added to mesh_routes.py in 15f2b6c but missed in
the WebSocket serializer, sending undefined for coverage on every WS
push. Also gates the initial health_update broadcast on composite > 0
and infra_total > 0 to prevent partially-initialized HealthScore from
overwriting good REST-fetched data on the frontend.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- /api/env/active: direct DB queries for fires, nws_alerts, quake_events
instead of env_store.get_active() (which depends on live NATS data)
- /api/env/swpc: reads band_conditions_broadcasts table, returns ratings
with slot label (Day/Night Propagation) derived from Mountain Time
- Frontend: replace RFPropagationCard (SFI/Kp/R/S/G charts) with
BandConditionsCard showing 4-band Good/Fair/Poor ratings
- Remove unused recharts dependency from Dashboard.tsx
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>