Commit graph

5 commits

Author SHA1 Message Date
Matt Johnson
c5aa0e1f42 Fix event-loop starvation, MeshCore stability, config-page hardening
- mesh_data_store.py / env/store.py: make refresh() async, offload blocking
  polls via asyncio.to_thread/gather so 7 lockstep sources no longer starve
  the shared event loop.
- main.py: gather pollers concurrently + set_default_executor thread pool.
- Dockerfile / docker-compose.yml: healthcheck now curls the dashboard for a
  real liveness signal instead of a process-exists check.
- transport/meshcore_transport.py: MeshCore keepalive loop (get_time() every
  120s), reconnect re-arm (_post_reconnect_setup_async from
  _on_connect_event), and MC channel-name normalization
  (_resolve_mc_channel_idx strips a leading #).
- dashboard-frontend: MeshCoreConnection.tsx config-page hardening, new
  ErrorBoundary component, wired into App.tsx.
- tests: fix ~40 call sites broken by refresh() becoming async (
  test_generic_http.py, test_store_received_delta.py,
  test_store_wzdx_persist.py) by wrapping with asyncio.run(), matching this
  suite's existing convention for calling async code from sync test
  functions. Verified: all 40 tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-16 01:24:45 +00:00
77e057ae86
fix(wzdx): persist current work zones to traffic_events via dedicated ingest (#109)
The WZDx daily summary + DM query count from traffic_events, but work
zones weren't landing there: wzdx rode the generic _delta_emit path,
which silent-seeds the seen-set and returns before the decider's INSERT
on the cold-start first poll, so the current zone set never persisted
(summary would count ~0). Add a dedicated _ingest_wzdx (mirroring the
fires ingest) that UPSERTs every current coalesced zone into
traffic_events each poll (persist-only, last_broadcast_at=NULL, no emit,
no broadcast) and reconciles zones that drop out of the feed (never wipes
on an empty/failed fetch). Per-event work-zone broadcast stays suppressed
(the decider's work_zone gate is untouched). Retargets 4 tests in
test_store_received_delta.py that used a fake 'wzdx' source to exercise
the generic gate onto a neutral routing name.

Co-authored-by: Matt Johnson <mj@k7zvx.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 20:08:13 -06:00
fcb24b9d3d
fix(roads511): stable external_id -> durably pre-seedable (belt + suspenders) (#51)
roads511 emitted external_id=None, so it couldn't be durably pre-seeded from
the persistent tables (only wzdx/usgs_quake were) — it relied solely on the
in-memory first-poll seed. Thread a stable external_id="511_{itd_id}" through
consistently so it joins the durable layer:

- env/roads511.py: _parse_event raw event + to_event both carry
  external_id="511_{id}" (== event_id). Flips _seen_key to the ext: branch and
  makes the incident decider persist traffic_events(source='511', external_id)
  — which ALSO restores the decider's own dedup (external_id=None was the
  original roads511 leak cause).
- env/store.py _seed_from_persistent: add a "511" spec (seed from
  traffic_events where source='511', by external_id) mirroring wzdx; shared
  _key_ext helper so keys can't drift.
- consistency proven byte-identical (raw _seen_key == pre-seed key ==
  511\x1eext:511_{id}); durable-preseed + regression tests added.

Live DB: 0 source='511' rows yet (flip recent) -> durability engages as native
rows accumulate; layer-2 in-memory seed covers the interim (atomic fetch).
Central-era itd_511 rows use a different keyspace, intentionally not covered.
Suite at 10-failure baseline (1716 passed).

Co-authored-by: Matt Johnson <mj@k7zvx.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 20:38:48 -06:00
7ec0883b44
fix(env): leak-proof received-delta — durable persistent baseline + non-empty seed guard (#44)
Two live backlog-broadcast leaks traced to the in-memory first-poll seed:
(1) incremental-fetch adapters (wzdx: registry tick [0 events] then feeds
tick [many]) got marked _seeded on the EMPTY first tick, so the real batch
next tick all looked "new" and broadcast; (2) in-memory seed lost on restart.

Fix — durable baseline + guard:
- _seed_from_persistent() at store init: pre-load already-received item keys
  from the persistent hazard tables into self._seen, so nothing ever received
  can re-broadcast (immune to fetch staging + restart). Only sources whose
  native emit key PROVABLY equals a persistent key are durably seeded:
  wzdx (traffic_events.external_id) + usgs_quake (quake_events.event_id).
  Resilient (per-table try/except; missing table -> skip).
- _seen_key() now namespaces by evt["source"] (matches persistent tables),
  via shared _key_ext/_key_eid helpers used by both seed and live emit so
  they can't drift.
- non-empty-seed guard: _ingest marks only sources that carried >=1 event
  this poll as _seeded -> an empty first tick can never seed-then-leak. This
  is the root-cause fix; covers all adapters (roads511/traffic fetch
  atomically per tick, so the guard fully protects them).
- storage untouched (self._events populated for every event); Central
  path/deciders untouched.

Live-DB verified: seed pre-loads 784 wzdx + 8 quake keys -> a live wzdx poll
of 784 known zones broadcasts 0. +6 tests (incremental staging, restart,
persistent-preseed, fresh-DB fallback); suite at 10-failure baseline.

Co-authored-by: Matt Johnson <mj@k7zvx.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 15:18:48 -06:00
0c76a806b8
feat(env): native adapters broadcast only newly-RECEIVED items (no backlog) (#43)
Replace the native path's "scan accumulated state + suppress what we've
already broadcast" model with "broadcast only what newly arrived from the
API this poll." Storage is unchanged (self._events + firms_pixels etc. are
populated for EVERY received item, so the LLM/get_active backlog is intact);
only the BROADCAST decision changes.

- env/store.py: per-adapter in-memory seen-set (_seen) + _seeded. First
  data-bearing poll for an adapter seeds keys and emits NOTHING (that batch
  is pre-existing backlog); later polls emit only keys not seen before.
  Restart => empty sets => next poll re-seeds silently. Structurally
  impossible to broadcast backlog on cold start / restart / re-enable.
  Key = external_id -> event_id -> content hash, namespaced per adapter.
  self._events[key]=evt still runs unconditionally (storage preserved).
- Fixes the ~175 (roads511) / ~782 (wzdx) cold-start bursts AND the latent
  quake/nws version (they only looked safe because Central pre-populated
  their broadcast tables).
- env/satpass.py: broadcast on AOS IMMINENCE (now < aos <= now+lead,
  broadcast_lead_seconds default 3600), future-only; window_hours still
  governs prediction depth. Strict norad_ids post-filter + fixed
  _parse_norad_ids char-iteration bug (cause of GOES/METEOR leak).
- Central path + broadcast-state tables untouched (native-only gate).

13 new tests; full suite at 10-failure baseline (1697 passed).

Co-authored-by: Matt Johnson <mj@k7zvx.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 14:38:40 -06:00