Weather alerts ran ~250-310 chars and were blind-sliced to 200 in the
central consumer, silently dropping storm motion + the impacted-town
list. The town list was also pre-capped to 80 chars at parse time,
destroying the middle/end of the storm path before formatting.
- nws_handler: preserve the full impacted-town list; when the message
overflows one mesh packet, sample the path (first -> middle -> last)
instead of truncating the tail, so both path endpoints survive
- consumer: pass precomposed titles through verbatim (no [:200] chop)
- adapter_config: add nws.single_packet_max_chars (default 200)
- tests: path-sampling + short-list coverage
Co-authored-by: Matt Johnson <mj@k7zvx.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Remove the nws_handler broadcast_severities/warning_suffix_promotes
pre-filter (GATE A) that dropped sub-Severe NWS products before the
pipeline. All NWS alerts now normalize to routine/priority/immediate
(map_severity) and breadth is governed solely by the per-toggle
dispatcher threshold. Warning-class categories are promoted to
immediate so a wrong/missing CAP severity int cant under-rank a real
warning. broadcast_severities/warning_suffix_promotes are now inert
(marked deprecated). Fixes sub-Severe alerts (Special Weather
Statements / advisories) for Magic Valley / East Idaho never reaching
the mesh despite a routine toggle threshold.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- alert on any hazard touching a node (severity gate removed)
- alert regardless of position staleness; only skip nodes with no position
- snow tabled: grayed-out in GUI + skipped in correlator pending snowfall+elevation pipeline
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Preserve the live CT108 working-tree state (the running container is already
built and is unaffected by this commit).
Canonical build tree (work/ — what ships):
- work/meshai/config.py : ConnectionConfig watchdog reconnect knobs
- work/meshai/connector.py : watchdog link-state, socket-based liveness,
active_probe, in-place reconnect
- work/meshai/main.py : connection supervisor (watchdog) task
- work/Dockerfile : healthcheck also asserts /tmp/meshai.link=up
- work/docker-compose.yml : matching healthcheck change
Root tree (stale duplicate, earlier reconnect iteration — preserved for fidelity):
- meshai/config.py, meshai/connector.py, meshai/main.py
- Dockerfile, docker-compose.yml
New (root only, NOT under work/, currently unimported / not in build):
- meshai/notifications/pipeline/severity_router.py
Excluded: all *.bak / *.bak2 backup artifacts.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Passes not happening today now show "tomorrow" or "Mon Jun 17" so they
aren't mistaken for past events. Single-observer consolidations now show
the observer name as region context, e.g. "(Treasure Valley)".
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
pyproject.toml references readme = "README.md" and the Dockerfile
COPYs it for pip install -e. With build context set to work/, the
file must exist there.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Move all application source (meshai/, dashboard-frontend/, tests/,
config/, docs/, Dockerfile, etc.) into work/ directory
- Add Node.js multi-stage build to Dockerfile for frontend compilation;
remove compiled static assets from git tracking
- Fix satpass missing time windows: consolidation was splitting wire on
newline and only putting line 1 in event.title, dropping the time
window line that the composer uses for precomposed broadcasts
- Fix satpass burst flooding: stagger consolidation timers (+60s per
pending pass) so Central batch publishes don't blast the mesh
- Update CI workflow build context to work/
- Anchor lib/ and data/ gitignore patterns to repo root to prevent
false matches on nested directories
- Add dashboard-frontend/node_modules/ to .dockerignore
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Passes whose AOS is more than max_aos_horizon_hours (default 24) in the
future are now rejected in the handler. Prevents stale predictions
republished via NATS LAST_PER_SUBJECT from broadcasting passes that are
too far out to be actionable. Complements the existing los < now guard
which catches passes that already ended.
New adapter_config key: satpass.max_aos_horizon_hours (int, default 24,
0 = disabled).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Fire events carried _severity_override="immediate" which zeroed the
dispatcher cooldown and skipped the Grouper coalescer. This meant fire
had no rate control in normal live operation. Drain-mode pacer handles
reconnect bursts; this change closes the live-operation gap so fire
obeys the toggle cooldown_seconds like every other family.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Satellite passes were broadcasting 3x (once per observer: Filer, Boise,
Idaho Falls). Now accumulates all observers for the same satellite+hour
into satpass_pending, waits 5s for stragglers, then emits one consolidated
broadcast showing entry→exit sweep (e.g. "Filer→Idaho Falls").
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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>