- 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>
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>
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>
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>
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>