Commit graph

373 commits

Author SHA1 Message Date
Ubuntu
de69a9fd57 feat(satpass): consolidate per-observer broadcasts into single regional pass
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>
2026-06-15 22:52:25 +00:00
Ubuntu
20cae52408 merge: fire spam drain pacer (de50414) into main
Fixes post-reconnect fire broadcast spam: severity downgrade
(immediate→priority), FIFO output pacer (≤1/min), drain mode
with per-IrwinID decision pass on NATS backlog catch-up.
2026-06-15 21:37:56 +00:00
Ubuntu
de50414471 fix(drain): add missing asyncio import for call_later
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-15 21:26:32 +00:00
Ubuntu
9ecd8652e1 fix(drain): add 30s timeout for empty-backlog drain exit
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>
2026-06-15 21:25:09 +00:00
Ubuntu
af4d2a3697 fix(drain): msg.metadata is a property, not a method
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>
2026-06-15 21:22:06 +00:00
Ubuntu
2f677e85a1 fix(fire): drain-mode pacer to prevent post-reconnect broadcast spam
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>
2026-06-15 21:19:00 +00:00
Claude
1557f3f5b7 merge: satpass compass fallback (cb8a2af) into main 2026-06-13 16:36:30 +00:00
Claude
cb8a2af075 fix(satpass): fall back to raw azimuth degrees for compass directions
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>
2026-06-13 15:20:47 +00:00
Claude
e2e065dda0 merge: satpass Tier1+2, fire recency, tombstone path, guard fix into main 2026-06-13 08:00:37 +00:00
Claude
2318f76015 fix(satpass): staleness guard — reject passes whose window already ended
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>
2026-06-13 01:10:58 +00:00
Claude
f94cf20953 fix(satpass): coerce norad_ids to int set at comparison site
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>
2026-06-12 23:36:18 +00:00
Claude
17808c3d65 feat(satpass): GUI safety controls panel — armed-state banner, dry-run toggle, rate cap
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>
2026-06-12 23:26:08 +00:00
Claude
e903444356 fix(satpass): broadcast safety controls — opt-in filter, rate cap, dry-run, wire format
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>
2026-06-12 23:00:03 +00:00
0cb175d65b fix: correct satpass_handler wire field names + add pass. category prefix
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>
2026-06-12 20:55:01 +00:00
199929ff1c fix: GUI satpass enabled toggle now persists through adapter-config API
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>
2026-06-12 20:16:07 +00:00
116e66369e test: satpass event path — wire adapter routing + enabled gate
13 tests covering the full sat event routing chain:

- CENTRAL_ADAPTER_TO_SOURCE maps celestrak_tle, n2yo_visualpasses,
  and satpass_predict to 'satpass' (3 tests)
- No stale adapter names (sat_passes, sat_tles, sat_tle) in map
- TLE handler inserts sat_tles row when enabled (integration)
- TLE handler drops when satpass.enabled=false
- Pass handler accepts n2yo_visualpasses and satpass_predict envelopes
- Pass handler rejects celestrak_tle envelope
- REGISTRY key is min_elevation (not min_elevation_deg)
- Handler source reads min_elevation (not min_elevation_deg)
- Consumer dispatch source has correct wire names

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-12 17:19:52 +00:00
be4443feb9 fix: wire adapter name mismatch in satpass event path
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>
2026-06-12 17:19:44 +00:00
4a17c7b702 fix: add satpass to region rewrite table in _subjects_for()
_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>
2026-06-12 15:33:25 +00:00
aa2838fee3 fix: register satpass adapter on EnvironmentalConfig
_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>
2026-06-12 07:39:44 +00:00
5418ed1f65 feat(satpass): Tier 2 on-demand satellite pass predictions (!satpass command)
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>
2026-06-12 06:36:32 +00:00
886bb6510b fix: tombstone broadcast path — immediate severity + commit handles
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>
2026-06-12 05:48:25 +00:00
b5a8f5cf97 fix(fire_digest): trim lines to name + location, correct tail count after budget trim
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-12 04:39:57 +00:00
bf88bdc9e6 fix(fires): recency-only ordering, exclude contained/tombstoned in LLM context and digest
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-12 04:17:50 +00:00
fa79fa1c9e fix(env_reporter): fire context hybrid recent+largest, exclude tombstoned
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>
2026-06-12 03:14:14 +00:00
1f34f74e5a feat(satpass): add satellite pass GUI to Environment + Notifications pages
- 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>
2026-06-12 00:10:51 +00:00
782899d4b1 feat(satpass): add satellite pass alert family (tier 1)
- Add satpass_handler.py with observer/elevation/NORAD filters
- Subscribe to central.sat.pass.> in consumer.py
- Add satpass toggle family to config.py
- Add sat_pass category mapping in categories.py
- Add satpass config section in adapter_config/defaults.py
- Add 9 unit tests for handler logic

Wire format: 3-line LoRa-tight (sat name, AOS/LOS times, observer)
Dedup: hourly bucket per sat/observer
Severity: 60deg+=immediate, 45deg+=priority, else routine
2026-06-11 23:23:24 +00:00
meshai-fix
40ad40f277 fix(dispatcher): commit guards only after successful delivery (B13) + dedup suffix for WFIGS lifecycle updates 2026-06-11 17:44:02 +00:00
98e3fcf675 Merge branch 'feature/mesh-intelligence' 2026-06-10 21:03:08 +00:00
b36223146f fix: harden EnvironmentalStore adapter init — isolate failures
- 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>
2026-06-10 20:25:50 +00:00
3dd48b6337 fix: Carbon sweep — TownAnchors + GaugeSites cyan/slate → Carbon tokens
- 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>
2026-06-10 19:55:02 +00:00
72611cc148 feat: move Photon geocoder config to config.yaml — public komoot default, local override in deployment config 2026-06-10 19:49:26 +00:00
f65d1d2f59 fix: Carbon sweep — GaugeSites, TownAnchors, Notifications, Alerts — teal/cyan/blue → amber 2026-06-10 19:42:39 +00:00
bce11b2196 fix: Layout.tsx — logo container bg, Carbon nav colors, remove stale blue active states 2026-06-10 19:37:13 +00:00
e181d0c126 feat: MeshAI logo PNG in sidebar, favicon wired 2026-06-10 19:33:58 +00:00
768f6e72ac Merge branch 'feature/mesh-intelligence' 2026-06-10 15:44:54 +00:00
06a5cc23ef fix: Carbon theme — bump muted text tiers for legibility (#333/#444 → #666/#777)
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>
2026-06-10 15:44:48 +00:00
7de460804f Merge branch 'feature/mesh-intelligence' 2026-06-10 15:37:33 +00:00
d0bf298f89 feat: Carbon theme — amber accent, Inter/JetBrains Mono, sharp corners, traffic-light semantic colors across all components
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>
2026-06-10 15:37:27 +00:00
f054d80efc Merge branch 'feature/mesh-intelligence' 2026-06-10 14:37:49 +00:00
6b08cdc004 feat: apply Carbon theme — amber accent, Inter/JetBrains Mono, sharp corners, Lucide icons preserved
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>
2026-06-10 14:37:33 +00:00
c0ceddff08 Merge branch 'feature/mesh-intelligence' 2026-06-10 07:19:48 +00:00
88554eb252 feat: dashboard visual overhaul — amber/blue/danger accents, Inter + JetBrains Mono, sharp corners, tighter density
- 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>
2026-06-10 07:19:38 +00:00
d8867433a1 Merge branch 'feature/mesh-intelligence' 2026-06-10 06:53:59 +00:00
af51c51708 feat: dashboard layout — tabbed alerts/feed, tropo in middle row, fix emoji
- 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>
2026-06-10 06:53:48 +00:00
825df8a3a8 Merge branch 'feature/mesh-intelligence' 2026-06-10 06:15:43 +00:00
3e06aacc3f feat: hepburn tropo card — region selector dropdown persisted to adapter_config
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>
2026-06-10 06:15:36 +00:00
ea5b6f5306 Merge branch 'feature/mesh-intelligence' 2026-06-10 06:05:01 +00:00
6989a49f64 fix: periodic health broadcast missing pillars — add full score payload + validity gate
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>
2026-06-10 06:04:56 +00:00
6d39da9a31 Merge branch 'feature/mesh-intelligence' 2026-06-10 05:58:03 +00:00
a119de7d86 fix: ws health_update — add coverage pillar + gate broadcast on valid computed score
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>
2026-06-10 05:57:58 +00:00