'use client'; import React, { useState, useEffect } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; import { X, ExternalLink, Key, Shield, Radar, Globe, Satellite, Ship, Radio, Bot, Copy, Check, Network } from 'lucide-react'; const CURRENT_ONBOARDING_VERSION = '0.9.81-agentic-onboarding-1'; const STORAGE_KEY = `shadowbroker_onboarding_complete_v${CURRENT_ONBOARDING_VERSION}`; const LEGACY_STORAGE_KEY = 'shadowbroker_onboarding_complete'; const API_GUIDES = [ { name: 'OpenSky Network', icon: , required: true, description: 'Flight tracking with global ADS-B coverage. Provides real-time aircraft positions.', steps: [ 'Create a free account at opensky-network.org', 'Go to Dashboard → OAuth → Create Client', 'Copy your Client ID and Client Secret', 'Paste both into Quick Local Setup above or Settings → API Keys', ], url: 'https://opensky-network.org/index.php?option=com_users&view=registration', color: 'cyan', }, { name: 'AIS Stream', icon: , required: true, description: 'Real-time vessel tracking via AIS (Automatic Identification System).', steps: [ 'Register at aisstream.io', 'Navigate to your API Keys page', 'Generate a new API key', 'Paste it into Quick Local Setup above or Settings → API Keys', ], url: 'https://aisstream.io/authenticate', color: 'blue', }, ]; const FREE_SOURCES = [ { name: 'ADS-B Exchange', desc: 'Military & general aviation', icon: }, { name: 'USGS Earthquakes', desc: 'Global seismic data', icon: }, { name: 'CelesTrak', desc: '2,000+ satellite orbits', icon: }, { name: 'GDELT Project', desc: 'Global conflict events', icon: }, { name: 'RainViewer', desc: 'Weather radar overlay', icon: }, { name: 'OpenMHz', desc: 'Radio scanner feeds', icon: }, { name: 'RSS Feeds', desc: 'NPR, BBC, Reuters, AP', icon: }, { name: 'Yahoo Finance', desc: 'Defense stocks & oil', icon: }, ]; interface OnboardingModalProps { onClose: () => void; onOpenSettings: () => void; } const OnboardingModal = React.memo(function OnboardingModal({ onClose, onOpenSettings, }: OnboardingModalProps) { const [step, setStep] = useState(0); const [setupKeys, setSetupKeys] = useState({ OPENSKY_CLIENT_ID: '', OPENSKY_CLIENT_SECRET: '', AIS_API_KEY: '', }); const [setupSaving, setSetupSaving] = useState(false); const [setupMsg, setSetupMsg] = useState<{ type: 'ok' | 'err'; text: string } | null>(null); const [agentSecret, setAgentSecret] = useState(''); const [agentTier, setAgentTier] = useState<'restricted' | 'full'>('restricted'); const [agentMode, setAgentMode] = useState<'local' | 'remote'>('local'); const [agentLoading, setAgentLoading] = useState(false); const [agentMsg, setAgentMsg] = useState<{ type: 'ok' | 'err'; text: string } | null>(null); const [agentCopied, setAgentCopied] = useState(false); const [torStarting, setTorStarting] = useState(false); const [torAddress, setTorAddress] = useState(''); const handleDismiss = () => { localStorage.setItem(STORAGE_KEY, 'true'); localStorage.setItem(LEGACY_STORAGE_KEY, 'true'); onClose(); }; const handleOpenSettings = () => { localStorage.setItem(STORAGE_KEY, 'true'); localStorage.setItem(LEGACY_STORAGE_KEY, 'true'); onClose(); onOpenSettings(); }; const saveSetupKeys = async () => { const payload = Object.fromEntries( Object.entries(setupKeys).filter(([, value]) => value.trim()), ); if (!Object.keys(payload).length) { setSetupMsg({ type: 'err', text: 'Enter at least one API key first.' }); return; } setSetupSaving(true); setSetupMsg(null); try { const res = await fetch('/api/settings/api-keys', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), }); const data = await res.json().catch(() => ({})); if (!res.ok || data?.ok === false) { throw new Error(data?.detail || 'Could not save API keys.'); } setSetupKeys({ OPENSKY_CLIENT_ID: '', OPENSKY_CLIENT_SECRET: '', AIS_API_KEY: '' }); setSetupMsg({ type: 'ok', text: 'Keys saved locally. Restart or refresh feeds to use them.' }); } catch (error) { setSetupMsg({ type: 'err', text: error instanceof Error ? error.message : 'Could not save API keys.', }); } finally { setSetupSaving(false); } }; const agentEndpoint = agentMode === 'local' ? 'http://localhost:8000' : torAddress || ''; const agentSnippet = [ `SHADOWBROKER_URL=${agentEndpoint}`, agentSecret ? `SHADOWBROKER_KEY=${agentSecret}` : 'SHADOWBROKER_KEY=', `SHADOWBROKER_ACCESS=${agentTier}`, '', '# FIRST: load available tools', `GET ${agentEndpoint}/api/ai/tools`, '', '# Auth: HMAC-SHA256 signed requests.', '# Restricted = read-only telemetry. Full = can write when asked.', ].join('\n'); const remoteAgentNeedsTor = agentMode === 'remote' && !torAddress; // Issue #302 (tg12): the full HMAC secret no longer comes back from // GET /api/ai/connect-info. We fetch metadata + the masked fingerprint // first; if the operator has explicitly asked to see the key (the // ``reveal`` flag), we follow up with POST /api/ai/connect-info/reveal // (after a transparent POST /bootstrap if the secret hasn't been // minted yet) which carries the secret with strict no-store headers. const fetchAgentConnectInfo = async (reveal = true) => { setAgentLoading(true); setAgentMsg(null); try { // 1) GET metadata + masked fingerprint. const metaRes = await fetch('/api/ai/connect-info'); const metaData = await metaRes.json().catch(() => ({})); if (!metaRes.ok || metaData?.ok === false) { throw new Error(metaData?.detail || 'Could not prepare agent credentials.'); } setAgentTier(metaData.access_tier === 'full' ? 'full' : 'restricted'); // 2) Mint the secret if it isn't set yet — transparent, idempotent. let secretSet = !!metaData.hmac_secret_set; if (!secretSet) { const bootRes = await fetch('/api/ai/connect-info/bootstrap', { method: 'POST', }); const bootData = await bootRes.json().catch(() => ({})); if (!bootRes.ok || bootData?.ok === false) { throw new Error(bootData?.detail || 'Could not generate agent credentials.'); } secretSet = !!bootData.hmac_secret_set; } // 3) If the caller asked to see the secret, fetch it explicitly. // Otherwise the masked fingerprint is enough for the UI. if (reveal && secretSet) { const revealRes = await fetch('/api/ai/connect-info/reveal', { method: 'POST', }); const revealData = await revealRes.json().catch(() => ({})); if (!revealRes.ok || revealData?.ok === false) { throw new Error(revealData?.detail || 'Could not reveal agent credentials.'); } setAgentSecret(revealData.hmac_secret || ''); } else { setAgentSecret(metaData.masked_hmac_secret || ''); } setAgentMsg({ type: 'ok', text: 'Agent key is ready. Copy it into your local or remote agent runtime.' }); } catch (error) { setAgentMsg({ type: 'err', text: error instanceof Error ? error.message : 'Could not prepare agent credentials.', }); } finally { setAgentLoading(false); } }; const saveAgentTier = async (tier: 'restricted' | 'full') => { setAgentTier(tier); setAgentMsg(null); try { const res = await fetch('/api/ai/connect-info/access-tier', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ tier }), }); const data = await res.json().catch(() => ({})); if (!res.ok || data?.ok === false) { throw new Error(data?.detail || 'Could not update agent access tier.'); } setAgentMsg({ type: 'ok', text: tier === 'full' ? 'Full access saved. The agent can write to the dashboard when authenticated.' : 'Restricted access saved. The agent can read telemetry but cannot write.', }); } catch (error) { setAgentMsg({ type: 'err', text: error instanceof Error ? error.message : 'Could not update agent access tier.', }); } }; const prepareTorAgentAddress = async () => { setTorStarting(true); setAgentMsg(null); try { const res = await fetch('/api/settings/tor/start', { method: 'POST' }); const data = await res.json().catch(() => ({})); if (!res.ok || data?.ok === false || !data?.onion_address) { throw new Error(data?.detail || 'Could not start Tor hidden service.'); } setTorAddress(data.onion_address); setAgentMsg({ type: 'ok', text: 'Tor is ready. The remote agent link is private to your local ShadowBroker node.', }); } catch (error) { setAgentMsg({ type: 'err', text: error instanceof Error ? error.message : 'ShadowBroker could not install or start Tor automatically. Check network access and try again.', }); } finally { setTorStarting(false); } }; const copyAgentSnippet = async () => { if (remoteAgentNeedsTor) { setAgentMsg({ type: 'err', text: 'Install Tor and create the remote link first, then copy the agent config.' }); return; } await navigator.clipboard.writeText(agentSnippet); setAgentCopied(true); setTimeout(() => setAgentCopied(false), 1600); }; return ( {/* Backdrop */} {/* Modal */}
e.stopPropagation()} > {/* Header */}

MISSION BRIEFING

FIRST-TIME SETUP
{/* Step Indicators */}
{['API Keys', 'AI Agent', 'Trust Modes', 'Free Sources'].map((label, i) => ( ))}
{/* Content */}
{step === 2 && (
T R U S T M O D E S

Real-time OSINT dashboard aggregating 12+ live intelligence sources. Flights, ships, satellites, earthquakes, conflicts, and more — all on one map.

These modes explain what lane the network is using. Set up the API keys first, then use this screen to understand public mesh versus private Wormhole paths.

8 Sources Work Immediately

Military aircraft, satellites, earthquakes, global conflicts, weather radar, radio scanners, news, and market data all work out of the box — no keys needed.

TRUST MODES

PUBLIC / DEGRADED — Meshtastic, APRS, and perimeter feeds. Observable and linkable.
PRIVATE / TRANSITIONAL — Wormhole lane is active. Gate chat runs on this lane, but metadata resistance is reduced until Reticulum is ready.
PRIVATE / STRONG — Wormhole and Reticulum are both ready. Dead Drop / DM requires this tier for the strongest privacy posture.

Public mesh is not private just because Wormhole exists. Use Wormhole when you want the private lane, and treat public mesh as public.

)} {step === 1 && (

STEP 1 - WHERE IS YOUR AGENT?

STEP 2 - WHAT CAN IT DO?

STEP 3 - COPY THIS INTO YOUR AGENT

Generate a local key, then copy these variables into OpenClaw, Hermes, or another HMAC agent.

{remoteAgentNeedsTor && (

TOR REQUIRED FOR REMOTE AGENTS

ShadowBroker will install or use Tor locally, then create a private .onion link for this backend.

)}
                      {agentSnippet}
                    
{agentMsg && (

{agentMsg.text}

)}

Remote agent access uses the signed HTTP API over Tor. Wormhole uses the same Tor/Arti transport lane when it is available; MLS-native agent transport is still planned.

)} {step === 0 && (

START HERE

OpenSky Network and AIS Stream are the free keys that make ShadowBroker useful immediately: live aircraft and vessel tracking. Paste them below or use Settings later; secrets stay on the local backend.

QUICK LOCAL SETUP

Paste keys here once. ShadowBroker stores them server-side only and never displays the secret back in the browser.

{[ ['OPENSKY_CLIENT_ID', 'OpenSky Client ID'], ['OPENSKY_CLIENT_SECRET', 'OpenSky Client Secret'], ['AIS_API_KEY', 'AIS Stream API Key'], ].map(([key, label]) => ( setSetupKeys((prev) => ({ ...prev, [key]: event.target.value })) } placeholder={label} className="w-full bg-[var(--bg-primary)] border border-[var(--border-primary)] px-3 py-2 text-sm text-[var(--text-primary)] font-mono outline-none focus:border-cyan-500/70 placeholder:text-[var(--text-muted)]/60" autoComplete="off" /> ))} {setupMsg && (

{setupMsg.text}

)}
{API_GUIDES.map((api) => (
{api.icon} {api.name} REQUIRED
GET KEY

{api.description}

    {api.steps.map((s, i) => (
  1. {i + 1}. {s}
  2. ))}
))}
)} {step === 3 && (

These data sources are completely free and require no API keys. They activate automatically on launch, while OpenSky and AIS Stream unlock the richer live aviation and maritime experience.

{FREE_SOURCES.map((src) => (
{src.icon} {src.name}

{src.desc}

))}
)}
{/* Footer */}
{[0, 1, 2, 3].map((i) => (
))}
{step < 3 ? ( ) : ( )}
); }); export function useOnboarding() { const [showOnboarding, setShowOnboarding] = useState(false); useEffect(() => { const done = localStorage.getItem(STORAGE_KEY); if (!done) { setShowOnboarding(true); } }, []); return { showOnboarding, setShowOnboarding }; } export default OnboardingModal;