mirror of
https://github.com/BigBodyCobain/Shadowbroker.git
synced 2026-07-09 21:58:41 +02:00
Harden v0.9.75 wormhole node sync and telemetry panels
Add Tor/onion runtime wiring and faster Infonet node status refresh. Keep node bootstrap state clearer across Docker and local runtimes. Use selected aircraft trail history for cumulative tracked-aircraft emissions.
This commit is contained in:
@@ -11,7 +11,6 @@ import {
|
||||
} from '@/mesh/infonetEconomyClient';
|
||||
import { generateNodeKeys, getNodeIdentity } from '@/mesh/meshIdentity';
|
||||
import {
|
||||
DEFAULT_INFONET_SEED_URL,
|
||||
fetchInfonetNodeStatusSnapshot,
|
||||
setInfonetNodeEnabled,
|
||||
type InfonetNodeStatusSnapshot,
|
||||
@@ -57,9 +56,12 @@ export default function BootstrapView({ marketId, onBack }: BootstrapViewProps)
|
||||
const nodeEnabled = Boolean(nodeStatus?.node_enabled);
|
||||
const nodeMode = String(nodeStatus?.node_mode || 'participant').toUpperCase();
|
||||
const syncOutcome = String(nodeStatus?.sync_runtime?.last_outcome || 'idle').toLowerCase();
|
||||
const seedPeerCount = Number(nodeStatus?.bootstrap?.default_sync_peer_count || 0);
|
||||
const seedPeerCount = Number(
|
||||
nodeStatus?.bootstrap?.bootstrap_seed_peer_count ?? nodeStatus?.bootstrap?.default_sync_peer_count ?? 0,
|
||||
);
|
||||
const syncPeerCount = Number(nodeStatus?.bootstrap?.sync_peer_count || 0);
|
||||
const lastPeerUrl = String(nodeStatus?.sync_runtime?.last_peer_url || '').trim();
|
||||
const privateTransportRequired = Boolean(nodeStatus?.private_transport_required);
|
||||
|
||||
const toggleNode = useCallback(async (enabled: boolean) => {
|
||||
setNodeToggleBusy(true);
|
||||
@@ -146,8 +148,10 @@ export default function BootstrapView({ marketId, onBack }: BootstrapViewProps)
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-2 text-xs">
|
||||
<div>
|
||||
<div className="text-gray-500">Default Seed</div>
|
||||
<div className="text-cyan-300 font-mono break-all">{DEFAULT_INFONET_SEED_URL}</div>
|
||||
<div className="text-gray-500">Transport</div>
|
||||
<div className="text-cyan-300 font-mono break-all">
|
||||
{privateTransportRequired ? 'ONION / RNS ONLY' : 'CLEARNET DEV OVERRIDE'}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-gray-500">Local Node</div>
|
||||
@@ -158,15 +162,15 @@ export default function BootstrapView({ marketId, onBack }: BootstrapViewProps)
|
||||
<div>
|
||||
<div className="text-gray-500">Sync Path</div>
|
||||
<div className="text-white font-mono">
|
||||
{syncPeerCount} peers / {seedPeerCount} default
|
||||
{syncPeerCount} peers / {seedPeerCount} seeds
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-3 flex flex-col md:flex-row md:items-center gap-3">
|
||||
<div className="flex-1 text-[11px] text-gray-500 leading-relaxed">
|
||||
{nodeEnabled
|
||||
? `Public chain sync is ${syncOutcome || 'active'}${lastPeerUrl ? ` via ${lastPeerUrl}` : ''}.`
|
||||
: 'Start a local participant node to pull from the default seed and help carry the public Infonet chain while this backend is running.'}
|
||||
? `Infonet sync is ${syncOutcome || 'active'}${lastPeerUrl ? ` via ${lastPeerUrl}` : ''}.`
|
||||
: 'Start a local participant node to sync through available Wormhole onion/RNS peers while this backend is running.'}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -3,6 +3,11 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { X } from 'lucide-react';
|
||||
import {
|
||||
fetchInfonetNodeStatusSnapshot,
|
||||
setInfonetNodeEnabled,
|
||||
startTorHiddenService,
|
||||
} from '@/mesh/controlPlaneStatusClient';
|
||||
import InfonetShell from './InfonetShell';
|
||||
|
||||
interface InfonetTerminalProps {
|
||||
@@ -28,6 +33,33 @@ export default function InfonetTerminal({
|
||||
return () => window.removeEventListener('keydown', handler);
|
||||
}, [isOpen, onClose]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) return;
|
||||
let cancelled = false;
|
||||
|
||||
const connectParticipantNode = async () => {
|
||||
try {
|
||||
const nodeStatus = await fetchInfonetNodeStatusSnapshot(true).catch(() => null);
|
||||
if (cancelled || nodeStatus?.node_enabled) return;
|
||||
|
||||
const torStatus = await startTorHiddenService().catch(() => null);
|
||||
if (cancelled || !torStatus?.running || !torStatus?.onion_address) return;
|
||||
|
||||
await setInfonetNodeEnabled(true);
|
||||
if (!cancelled) {
|
||||
await fetchInfonetNodeStatusSnapshot(true).catch(() => null);
|
||||
}
|
||||
} catch {
|
||||
// Remote/shared viewers may not have local-operator rights. Leave manual controls intact.
|
||||
}
|
||||
};
|
||||
|
||||
void connectParticipantNode();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [isOpen]);
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{isOpen && (
|
||||
|
||||
@@ -7,6 +7,7 @@ import React, { useEffect, useRef, useCallback } from 'react';
|
||||
import WikiImage from '@/components/WikiImage';
|
||||
import type { SelectedEntity, RegionDossier, FimiData } from "@/types/dashboard";
|
||||
import { useDataKeys } from '@/hooks/useDataStore';
|
||||
import { API_BASE } from '@/lib/api';
|
||||
import { lookupShodanHost } from '@/lib/shodanClient';
|
||||
import type { ShodanHost } from '@/types/shodan';
|
||||
|
||||
@@ -291,6 +292,8 @@ function estimateObservedEmissions(flight: any): {
|
||||
const fuelGph = Number(flight?.emissions?.fuel_gph);
|
||||
const co2KgPerHour = Number(flight?.emissions?.co2_kg_per_hour);
|
||||
const trail = Array.isArray(flight?.trail) ? (flight.trail as FlightTrailPoint[]) : [];
|
||||
const isTrackedAircraft = flight?.type === 'tracked_flight' || Boolean(flight?.alert_category);
|
||||
const minimumObservedHours = isTrackedAircraft ? 1 / 60 : 5 / 60;
|
||||
if (!Number.isFinite(fuelGph) || !Number.isFinite(co2KgPerHour)) {
|
||||
return null;
|
||||
}
|
||||
@@ -301,7 +304,7 @@ function estimateObservedEmissions(flight: any): {
|
||||
.sort((a, b) => a - b);
|
||||
if (timestamps.length >= 2) {
|
||||
const elapsedHours = (timestamps[timestamps.length - 1] - timestamps[0]) / 3600;
|
||||
if (Number.isFinite(elapsedHours) && elapsedHours >= 5 / 60) {
|
||||
if (Number.isFinite(elapsedHours) && elapsedHours >= minimumObservedHours) {
|
||||
let distance = 0;
|
||||
let previous: { lat: number; lng: number } | null = null;
|
||||
for (const point of trail) {
|
||||
@@ -336,7 +339,7 @@ function estimateObservedEmissions(flight: any): {
|
||||
) {
|
||||
const flownNm = distanceNm(origin, current);
|
||||
const elapsedHours = flownNm / speedKnots;
|
||||
if (Number.isFinite(elapsedHours) && elapsedHours >= 5 / 60 && elapsedHours <= 18) {
|
||||
if (Number.isFinite(elapsedHours) && elapsedHours >= minimumObservedHours && elapsedHours <= 18) {
|
||||
return {
|
||||
fuelGallons: Math.round(fuelGph * elapsedHours),
|
||||
co2Kg: Math.round(co2KgPerHour * elapsedHours),
|
||||
@@ -405,6 +408,7 @@ function NewsFeedInner({ selectedEntity, regionDossier, regionDossierLoading, on
|
||||
'airports', 'last_updated', 'threat_level',
|
||||
] as const);
|
||||
const [isMinimized, setIsMinimized] = useState(false);
|
||||
const [selectedFlightTrail, setSelectedFlightTrail] = useState<FlightTrailPoint[]>([]);
|
||||
const [expandedIndexes, setExpandedIndexes] = useState<number[]>([]);
|
||||
const [fimiExpanded, setFimiExpanded] = useState(false);
|
||||
const [aiSummaryOpen, setAiSummaryOpen] = useState(false);
|
||||
@@ -453,6 +457,53 @@ function NewsFeedInner({ selectedEntity, regionDossier, regionDossierLoading, on
|
||||
return flight?.model;
|
||||
})();
|
||||
const { imgUrl: aircraftImgUrl, wikiUrl: aircraftWikiUrl, loading: aircraftImgLoading } = useAircraftImage(selectedFlightModel);
|
||||
|
||||
useEffect(() => {
|
||||
const flightSelectionTypes = new Set([
|
||||
'flight',
|
||||
'commercial_flight',
|
||||
'private_flight',
|
||||
'private_ga',
|
||||
'private_jet',
|
||||
'military_flight',
|
||||
'tracked_flight',
|
||||
]);
|
||||
if (!selectedEntity || !flightSelectionTypes.has(selectedEntity.type)) {
|
||||
setSelectedFlightTrail([]);
|
||||
return;
|
||||
}
|
||||
|
||||
const trailId = String(selectedEntity.id || '').trim();
|
||||
if (!trailId) {
|
||||
setSelectedFlightTrail([]);
|
||||
return;
|
||||
}
|
||||
|
||||
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([]);
|
||||
});
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [selectedEntity?.id, selectedEntity?.type]);
|
||||
|
||||
const withSelectedTrail = useCallback((flight: any) => {
|
||||
if (!flight || selectedFlightTrail.length < 2) return flight;
|
||||
const selectedId = String(selectedEntity?.id || '').trim().toLowerCase();
|
||||
const flightId = String(flight.icao24 || '').trim().toLowerCase();
|
||||
if (!selectedId || !flightId || selectedId !== flightId) return flight;
|
||||
return { ...flight, trail: selectedFlightTrail };
|
||||
}, [selectedEntity?.id, selectedFlightTrail]);
|
||||
|
||||
const [shodanDetail, setShodanDetail] = useState<ShodanHost | null>(null);
|
||||
const [shodanLoading, setShodanLoading] = useState(false);
|
||||
const [shodanError, setShodanError] = useState<string | null>(null);
|
||||
@@ -666,6 +717,7 @@ function NewsFeedInner({ selectedEntity, regionDossier, regionDossierLoading, on
|
||||
if (selectedEntity?.type === 'tracked_flight') {
|
||||
const flight = data?.tracked_flights?.find((f: any) => f.icao24 === selectedEntity.id);
|
||||
if (flight) {
|
||||
const flightForEmissions = withSelectedTrail(flight);
|
||||
const callsign = flight.callsign || "UNKNOWN";
|
||||
const alertColorMap: Record<string, string> = {
|
||||
'#ff1493': 'text-[#ff1493]', pink: 'text-[#ff1493]', red: 'text-red-400', yellow: 'text-yellow-400',
|
||||
@@ -851,7 +903,7 @@ function NewsFeedInner({ selectedEntity, regionDossier, regionDossierLoading, on
|
||||
<span className={`text-xs font-bold ${flight.squawk === '7700' ? 'text-red-400 animate-pulse' : flight.squawk === '7600' ? 'text-yellow-400' : 'text-[var(--text-primary)]'}`}>{flight.squawk}{flight.squawk === '7700' ? ' ⚠ EMERGENCY' : flight.squawk === '7600' ? ' COMMS LOST' : ''}</span>
|
||||
</div>
|
||||
)}
|
||||
<EmissionsEstimateBlock flight={flight} />
|
||||
<EmissionsEstimateBlock flight={flightForEmissions} />
|
||||
{flight.alert_link && (
|
||||
<div className="flex justify-between items-center border-b border-[var(--border-primary)] pb-2">
|
||||
<span className="text-[var(--text-muted)] text-[10px]">REFERENCE</span>
|
||||
@@ -903,6 +955,7 @@ function NewsFeedInner({ selectedEntity, regionDossier, regionDossierLoading, on
|
||||
const flight = flightsList?.find((f: any) => f.icao24 === selectedEntity.id);
|
||||
|
||||
if (flight) {
|
||||
const flightForEmissions = withSelectedTrail(flight);
|
||||
const callsign = flight.callsign || "UNKNOWN";
|
||||
let airline = "UNKNOWN";
|
||||
const isPrivateFlight = selectedEntity.type === 'private_flight' || selectedEntity.type === 'private_jet';
|
||||
@@ -1107,7 +1160,7 @@ function NewsFeedInner({ selectedEntity, regionDossier, regionDossierLoading, on
|
||||
<span className="text-[var(--text-muted)] text-[10px]">ROUTE</span>
|
||||
<span className="text-cyan-400 text-xs font-bold">{flight.origin_name !== "UNKNOWN" ? `[${flight.origin_name}] → [${flight.dest_name}]` : "UNKNOWN"}</span>
|
||||
</div>
|
||||
<EmissionsEstimateBlock flight={flight} />
|
||||
<EmissionsEstimateBlock flight={flightForEmissions} />
|
||||
{flight.icao24 && (
|
||||
<div className="flex justify-between items-center border-b border-[var(--border-primary)] pb-2">
|
||||
<span className="text-[var(--text-muted)] text-[10px]">FLIGHT RECORD</span>
|
||||
|
||||
@@ -33,8 +33,8 @@ import {
|
||||
import { purgeBrowserContactGraph, purgeBrowserSigningMaterial, setSecureModeCached, getNodeIdentity, generateNodeKeys } from '@/mesh/meshIdentity';
|
||||
import { purgeBrowserDmState } from '@/mesh/meshDmWorkerClient';
|
||||
import {
|
||||
DEFAULT_INFONET_SEED_URL,
|
||||
fetchInfonetNodeStatusSnapshot,
|
||||
startTorHiddenService,
|
||||
type InfonetNodeStatusSnapshot,
|
||||
} from '@/mesh/controlPlaneStatusClient';
|
||||
import {
|
||||
@@ -263,6 +263,10 @@ export default function TopRightControls({
|
||||
await generateNodeKeys();
|
||||
}
|
||||
setActivatingPhase('peers');
|
||||
const torStatus = await startTorHiddenService();
|
||||
if (!torStatus?.running || !torStatus?.onion_address) {
|
||||
throw new Error(torStatus?.detail || 'Tor onion service did not start');
|
||||
}
|
||||
}
|
||||
|
||||
const res = await controlPlaneFetch('/api/settings/node', {
|
||||
@@ -833,9 +837,9 @@ export default function TopRightControls({
|
||||
: activatingPhase === 'peers' ? 'text-cyan-300'
|
||||
: 'text-green-300'
|
||||
}>
|
||||
{activatingPhase === 'keys' ? 'Connecting to default seed...'
|
||||
: activatingPhase === 'peers' ? 'Connecting to default seed...'
|
||||
: 'Default seed connected'}
|
||||
{activatingPhase === 'keys' ? 'Preparing onion transport...'
|
||||
: activatingPhase === 'peers' ? 'Finding bootstrap peers...'
|
||||
: 'Bootstrap peers ready'}
|
||||
</span>
|
||||
</div>
|
||||
{/* Step: Sync chain */}
|
||||
@@ -899,7 +903,7 @@ export default function TopRightControls({
|
||||
<div className="border border-cyan-500/20 bg-cyan-950/10 px-4 py-4 text-[10px] font-mono text-cyan-100 leading-[1.8]">
|
||||
Do you want to activate a node on this install?
|
||||
<div className="mt-2 text-[9px] text-cyan-200/70 normal-case tracking-normal">
|
||||
This turns on your local participant node and lets this install keep syncing the public Infonet chain from <span className="text-cyan-300">{DEFAULT_INFONET_SEED_URL}</span>.
|
||||
This turns on your local participant node and syncs Infonet only through available Wormhole onion/RNS peers. Clearnet bootstrap is disabled by default.
|
||||
</div>
|
||||
</div>
|
||||
{(bootstrapFailed || nodeStatusError || nodeToggleError) && (
|
||||
@@ -930,10 +934,10 @@ export default function TopRightControls({
|
||||
<div className="text-cyan-300 tracking-[0.18em]">BY CONTINUING YOU AGREE:</div>
|
||||
<ul className="mt-3 space-y-2 list-disc pl-5">
|
||||
<li>This install can keep a local copy of the public Infonet chain.</li>
|
||||
<li>Fresh installs pull from the bundled default seed at {DEFAULT_INFONET_SEED_URL}.</li>
|
||||
<li>Participant-node sync is public-facing unless you separately use obfuscated-lane features.</li>
|
||||
<li>Your backend may sync with configured or bundled bootstrap peers in the background.</li>
|
||||
<li>Wormhole provides gates (transitional private lane) and Dead Drop / DM (stronger private lane) as separate postures.</li>
|
||||
<li>Fresh installs do not use a clearnet Infonet seed.</li>
|
||||
<li>Participant-node sync requires an onion/RNS peer through Wormhole.</li>
|
||||
<li>Your backend may sync with configured private bootstrap peers in the background.</li>
|
||||
<li>Wormhole keeps Infonet, gates, Dead Drop, and DM traffic on the obfuscated lane.</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="text-[11px] font-mono uppercase tracking-[0.2em] text-cyan-300/80">
|
||||
|
||||
Reference in New Issue
Block a user