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>
100.100.100.100 resolves .echo6.mesh hostnames (including
central.echo6.mesh for the NATS bus). 1.1.1.1 provides fallback
for public DNS when MagicDNS is unavailable.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Without this branch, the environmental config dict falls through to the
bare else clause, storing a raw dict instead of an EnvironmentalConfig
dataclass. This causes AttributeError when accessing env_cfg.enabled or
any nested feed config attributes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add coverage pillar to /api/health response and _serialize_health_score
- Add coverage to MeshHealth.pillars TypeScript interface
- Add Coverage PillarBar between Utilization and Behavior
- Active Alerts panel: show high-severity env events (immediate/priority)
as fallback when mesh alerts are empty, with ENV badge
Issues 3 (Live Event Feed) and 4 (RF Propagation): diagnosed as
env feed configuration — SWPC adapter disabled, only ducting feed
loaded, /api/env/active returns 0 events. Not a code bug.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add _is_update() helper that checks CAP references field against
nws_alerts table. When any referenced CAP id has last_broadcast_at
set, the wire gets an Update: prefix via _render(prefix=). Applied
in both the new-alert and cold-start-race branches.
References field shape: list of dicts with identifier key containing
the superseded CAP id (urn:oid:...).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>