From 5ee4f8ecd7e7467a50471d4ef7c1c085223929bd Mon Sep 17 00:00:00 2001 From: BigBodyCobain <43977454+BigBodyCobain@users.noreply.github.com> Date: Wed, 6 May 2026 22:10:04 -0600 Subject: [PATCH] Stabilize Infonet private sync and selected telemetry --- backend/main.py | 66 +++++++++++++++++-- backend/routers/wormhole.py | 7 +- backend/services/fetchers/flights.py | 22 ++++--- backend/services/fetchers/military.py | 8 +++ .../InfonetTerminal/NetworkStats.tsx | 2 +- frontend/src/components/MaplibreViewer.tsx | 27 ++++---- frontend/src/components/MeshTerminal.tsx | 6 +- frontend/src/components/NewsFeed.tsx | 25 ++++--- 8 files changed, 119 insertions(+), 44 deletions(-) diff --git a/backend/main.py b/backend/main.py index 45c8b3e..29ff823 100644 --- a/backend/main.py +++ b/backend/main.py @@ -1084,6 +1084,7 @@ _WORMHOLE_PUBLIC_PROFILE_FIELDS = {"profile", "wormhole_enabled"} _PRIVATE_LANE_CONTROL_FIELDS = {"private_lane_tier", "private_lane_policy"} _PUBLIC_RNS_STATUS_FIELDS = {"enabled", "ready", "configured_peers", "active_peers"} _NODE_PUBLIC_EVENT_HOOK_REGISTERED = False +_INFONET_PRIVATE_TRANSPORT_LOCK = threading.Lock() def _current_node_mode() -> str: @@ -1172,6 +1173,50 @@ def _filter_infonet_sync_records(records: list[Any]) -> list[Any]: ] +def _ensure_infonet_private_transport_ready(reason: str = "") -> bool: + """Warm the local onion transport before private Infonet sync. + + Infonet may know about an onion seed before the Wormhole UI is opened. The + sync worker still needs Arti marked enabled and a ready SOCKS listener, so + do that lazily in the worker instead of making users manually open another + panel just to participate in the Infonet. + """ + if not _infonet_private_transport_required(): + return True + + try: + from services.wormhole_supervisor import _check_arti_ready + + if bool(get_settings().MESH_ARTI_ENABLED) and _check_arti_ready(): + return True + except Exception: + pass + + if not _INFONET_PRIVATE_TRANSPORT_LOCK.acquire(blocking=False): + return False + try: + from routers.ai_intel import _write_env_value + from services.tor_hidden_service import tor_service + from services.wormhole_supervisor import _check_arti_ready + + label = f" ({reason})" if reason else "" + logger.info("Infonet private transport warmup starting%s", label) + tor_result = tor_service.start(target_port=8000) + if tor_result.get("ok"): + _write_env_value("MESH_ARTI_ENABLED", "true") + get_settings.cache_clear() + if _check_arti_ready(): + logger.info("Infonet private transport ready%s", label) + return True + logger.warning("Infonet private transport warmup incomplete%s: %s", label, tor_result) + return False + except Exception as exc: + logger.warning("Infonet private transport warmup failed: %s", exc) + return False + finally: + _INFONET_PRIVATE_TRANSPORT_LOCK.release() + + def _configured_bootstrap_seed_peer_urls() -> list[str]: settings = get_settings() primary = str(getattr(settings, "MESH_BOOTSTRAP_SEED_PEERS", "") or "").strip() @@ -1487,6 +1532,12 @@ def _run_public_sync_cycle() -> SyncWorkerState: set_sync_state(updated) return updated + if _infonet_private_transport_required() and any( + str(getattr(record, "transport", "") or "").strip().lower() == "onion" + for record in peers + ): + _ensure_infonet_private_transport_ready("sync") + last_error = "sync failed" for record in peers: started = begin_sync( @@ -2295,7 +2346,13 @@ async def lifespan(app: FastAPI): _refresh_node_peer_store() if _node_runtime_supported(): if not _participant_node_enabled(): - set_sync_state(_set_node_sync_disabled_state()) + logger.info("Infonet participant auto-enabled for private seed sync") + _set_participant_node_enabled(True) + threading.Thread( + target=lambda: _ensure_infonet_private_transport_ready("startup"), + daemon=True, + name="infonet-private-transport-warmup", + ).start() _NODE_SYNC_STOP.clear() threading.Thread(target=_public_infonet_sync_loop, daemon=True).start() _kick_public_sync_background("startup") @@ -9662,10 +9719,9 @@ async def api_wormhole_leave(request: Request): updated = write_wormhole_settings(enabled=False) state = disconnect_wormhole(reason="leave_wormhole") - # Disable node participation when the user leaves the Wormhole. - from services.node_settings import write_node_settings - - write_node_settings(enabled=False) + # Leaving private DM mode must not disable Infonet participation. Infonet + # sync has its own private transport warmup and can remain connected to + # seed/peer nodes while MeshChat stays separately opt-in. return { "ok": True, diff --git a/backend/routers/wormhole.py b/backend/routers/wormhole.py index 0f27a03..16d6ca7 100644 --- a/backend/routers/wormhole.py +++ b/backend/routers/wormhole.py @@ -929,10 +929,9 @@ async def api_wormhole_leave(request: Request): updated = write_wormhole_settings(enabled=False) state = disconnect_wormhole(reason="leave_wormhole") - # Disable node participation when the user leaves the Wormhole. - from services.node_settings import write_node_settings - - write_node_settings(enabled=False) + # Leaving private DM mode must not disable Infonet participation. Infonet + # sync has its own private transport warmup and can remain connected to + # seed/peer nodes while MeshChat stays separately opt-in. return { "ok": True, diff --git a/backend/services/fetchers/flights.py b/backend/services/fetchers/flights.py index be955ac..c55c61d 100644 --- a/backend/services/fetchers/flights.py +++ b/backend/services/fetchers/flights.py @@ -256,7 +256,7 @@ PRIVATE_JET_TYPES = { # Flight trails state flight_trails = {} # {icao_hex: {points: [[lat, lng, alt, ts], ...], last_seen: ts}} _trails_lock = threading.Lock() -_MAX_TRACKED_TRAILS = 2000 +_MAX_TRACKED_TRAILS = 20000 def get_flight_trail(icao24: str) -> list: @@ -624,7 +624,7 @@ def _classify_and_publish(all_adsb_flights): # --- Trail Accumulation --- _TRAIL_INTERVAL_S = 60 # selected trails need enough resolution to show where unknown-route traffic came from - def _accumulate_trail(f, now_ts, check_route=True): + def _accumulate_trail(f, now_ts, attach_known_route_trail=False): hex_id = f.get("icao24", "").lower() if not hex_id: return 0, None @@ -637,12 +637,9 @@ def _classify_and_publish(all_adsb_flights): (f.get("origin_loc") and f.get("dest_loc")) or (_known_route_name(f.get("origin_name")) and _known_route_name(f.get("dest_name"))) ) - if check_route and has_known_route: - f["trail"] = [] - return 0, hex_id lat, lng, alt = f.get("lat"), f.get("lng"), f.get("alt", 0) if lat is None or lng is None: - f["trail"] = flight_trails.get(hex_id, {}).get("points", []) + f["trail"] = [] if has_known_route and not attach_known_route_trail else flight_trails.get(hex_id, {}).get("points", []) return 0, hex_id point = [round(lat, 5), round(lng, 5), round(alt, 1), round(now_ts)] if hex_id not in flight_trails: @@ -663,7 +660,10 @@ def _classify_and_publish(all_adsb_flights): trail_data["last_seen"] = now_ts if len(trail_data["points"]) > 200: trail_data["points"] = trail_data["points"][-200:] - f["trail"] = trail_data["points"] + # Keep known-route flights visually clean in the main payload; selected + # detail panels can still fetch this server-side trail to compute + # observed fuel/CO2 burn. + f["trail"] = [] if has_known_route and not attach_known_route_trail else trail_data["points"] return 1, hex_id now_ts = datetime.utcnow().timestamp() @@ -675,7 +675,9 @@ def _classify_and_publish(all_adsb_flights): tracked_snapshot = copy.deepcopy(latest_data.get("tracked_flights", [])) raw_flights_snapshot = list(latest_data.get("flights", [])) - # Skip trails for any flight with a known route; the route line replaces historical trail. + # Accumulate trails for every aircraft so selected details can estimate + # observed fuel/CO2 burn. Known-route flights keep an empty payload trail so + # the route line, not historical breadcrumbs, remains the visible map path. route_check_lists = [commercial_snapshot, private_jets_snapshot, private_ga_snapshot] always_trail_lists = [tracked_snapshot, military_snapshot] seen_hexes = set() @@ -683,14 +685,14 @@ def _classify_and_publish(all_adsb_flights): with _trails_lock: for flist in route_check_lists: for f in flist: - count, hex_id = _accumulate_trail(f, now_ts, check_route=True) + count, hex_id = _accumulate_trail(f, now_ts, attach_known_route_trail=False) trail_count += count if hex_id: seen_hexes.add(hex_id) for flist in always_trail_lists: for f in flist: - count, hex_id = _accumulate_trail(f, now_ts, check_route=True) + count, hex_id = _accumulate_trail(f, now_ts, attach_known_route_trail=False) trail_count += count if hex_id: seen_hexes.add(hex_id) diff --git a/backend/services/fetchers/military.py b/backend/services/fetchers/military.py index 55a3ea8..b9816d3 100644 --- a/backend/services/fetchers/military.py +++ b/backend/services/fetchers/military.py @@ -6,6 +6,7 @@ import time import requests from services.network_utils import fetch_with_curl from services.fetchers._store import latest_data, _data_lock, _mark_fresh +from services.fetchers.emissions import get_emissions_info from services.fetchers.plane_alert import enrich_with_plane_alert logger = logging.getLogger("services.data_fetcher") @@ -289,6 +290,13 @@ def fetch_military_flights(): remaining_mil = [] for mf in military_flights: enrich_with_plane_alert(mf) + model = mf.get("model") + if not model or str(model).strip().lower() in {"", "unknown"}: + model = mf.get("alert_type") or "" + if model: + emissions = get_emissions_info(model) + if emissions: + mf["emissions"] = emissions if mf.get("alert_category"): mf["type"] = "tracked_flight" tracked_mil.append(mf) diff --git a/frontend/src/components/InfonetTerminal/NetworkStats.tsx b/frontend/src/components/InfonetTerminal/NetworkStats.tsx index 0d6a3eb..29d7b8a 100644 --- a/frontend/src/components/InfonetTerminal/NetworkStats.tsx +++ b/frontend/src/components/InfonetTerminal/NetworkStats.tsx @@ -61,7 +61,7 @@ export default function NetworkStats() { const nodeColor = stats.syncOutcome === 'ok' ? 'text-green-400' : stats.syncOutcome === 'running' ? 'text-amber-400' : stats.nodeEnabled ? 'text-amber-400' : 'text-gray-600'; - const nodeLabel = stats.syncOutcome === 'ok' ? 'CONNECTED' + const nodeLabel = stats.syncOutcome === 'ok' ? 'SEED SYNCED' : stats.syncOutcome === 'running' ? 'SYNCING' : stats.syncOutcome === 'error' || stats.syncOutcome === 'fork' ? 'RETRYING' : stats.nodeEnabled ? 'WAITING' : 'OFFLINE'; diff --git a/frontend/src/components/MaplibreViewer.tsx b/frontend/src/components/MaplibreViewer.tsx index be60545..18b4761 100644 --- a/frontend/src/components/MaplibreViewer.tsx +++ b/frontend/src/components/MaplibreViewer.tsx @@ -274,7 +274,7 @@ function hasKnownRouteName(value?: string | null): boolean { function flightHasKnownRoute(entity: ReturnType, dynamicRoute: DynamicRoute | null): boolean { if (!entity) return false; - if (dynamicRoute?.orig_loc || dynamicRoute?.dest_loc) return true; + if (dynamicRoute?.orig_loc && dynamicRoute?.dest_loc) return true; if (!('origin_loc' in entity) && !('origin_name' in entity)) return false; const flight = entity as Flight; return Boolean( @@ -683,19 +683,24 @@ const MaplibreViewer = ({ const endpoint = isShip ? `${API_BASE}/api/trail/ship/${encodeURIComponent(trailId)}` : `${API_BASE}/api/trail/flight/${encodeURIComponent(trailId)}`; - fetch(endpoint) - .then((res) => (res.ok ? res.json() : null)) - .then((payload) => { - if (cancelled || !payload) return; - const points = parseTrailPoints(payload.trail, kind); - setSelectedTrailPoints(points.length >= 2 ? points : fallback); - }) - .catch(() => { - if (!cancelled) setSelectedTrailPoints(fallback); - }); + const refreshSelectedTrail = () => { + fetch(endpoint, { cache: 'no-store' }) + .then((res) => (res.ok ? res.json() : null)) + .then((payload) => { + if (cancelled || !payload) return; + const points = parseTrailPoints(payload.trail, kind); + setSelectedTrailPoints(points.length >= 2 ? points : fallback); + }) + .catch(() => { + if (!cancelled) setSelectedTrailPoints(fallback); + }); + }; + refreshSelectedTrail(); + const trailRefreshTimer = window.setInterval(refreshSelectedTrail, 30000); return () => { cancelled = true; + window.clearInterval(trailRefreshTimer); }; }, [selectedEntity, data, dynamicRoute]); diff --git a/frontend/src/components/MeshTerminal.tsx b/frontend/src/components/MeshTerminal.tsx index 391ca2c..b647ee2 100644 --- a/frontend/src/components/MeshTerminal.tsx +++ b/frontend/src/components/MeshTerminal.tsx @@ -5953,7 +5953,7 @@ export default function MeshTerminal({ isOpen, launchToken = 0, onClose, onDmCou PARTICIPANT NODE
- Backend bootstrap is configured; activate the participant node to sync the public testnet seed without Wormhole. + Backend bootstrap is configured; the participant node syncs the testnet seed over the private seed lane.
@@ -6008,10 +6008,10 @@ export default function MeshTerminal({ isOpen, launchToken = 0, onClose, onDmCou
- WORMHOLE OPTIONAL FOR NODE SYNC + PRIVATE SEED LANE
- Participant-node bootstrap, sync, and public chain hosting run on the backend lane without Wormhole. + Participant-node bootstrap, sync, and public chain hosting use the backend private seed lane.
Turn Wormhole on for gates, obfuscated inbox, and the stronger obfuscated lane only. diff --git a/frontend/src/components/NewsFeed.tsx b/frontend/src/components/NewsFeed.tsx index a8a2fae..15c8571 100644 --- a/frontend/src/components/NewsFeed.tsx +++ b/frontend/src/components/NewsFeed.tsx @@ -480,19 +480,24 @@ function NewsFeedInner({ selectedEntity, regionDossier, regionDossierLoading, on } let cancelled = false; - fetch(`${API_BASE}/api/trail/flight/${encodeURIComponent(trailId)}`, { cache: 'no-store' }) - .then((res) => (res.ok ? res.json() : null)) - .then((payload) => { - if (cancelled) return; - const trail = Array.isArray(payload?.trail) ? payload.trail as FlightTrailPoint[] : []; - setSelectedFlightTrail(trail); - }) - .catch(() => { - if (!cancelled) setSelectedFlightTrail([]); - }); + const refreshSelectedFlightTrail = () => { + fetch(`${API_BASE}/api/trail/flight/${encodeURIComponent(trailId)}`, { cache: 'no-store' }) + .then((res) => (res.ok ? res.json() : null)) + .then((payload) => { + if (cancelled) return; + const trail = Array.isArray(payload?.trail) ? payload.trail as FlightTrailPoint[] : []; + setSelectedFlightTrail(trail); + }) + .catch(() => { + if (!cancelled) setSelectedFlightTrail([]); + }); + }; + refreshSelectedFlightTrail(); + const trailRefreshTimer = window.setInterval(refreshSelectedFlightTrail, 30000); return () => { cancelled = true; + window.clearInterval(trailRefreshTimer); }; }, [selectedEntity?.id, selectedEntity?.type]);