'use client'; import { useState, useRef, useEffect, useCallback } from 'react'; import { createPortal } from 'react-dom'; import { Github, Download, AlertCircle, CheckCircle2, RefreshCw, ExternalLink, X, Terminal, Server, Copy, } from 'lucide-react'; import { API_BASE } from '@/lib/api'; import { controlPlaneFetch } from '@/lib/controlPlane'; import { checkDesktopUpdaterUpdate, classifyUpdateRuntime, getDesktopUpdateContext, getPreferredManualUpdateUrl, getUpdateAction, installDesktopUpdaterUpdate, type GitHubLatestRelease, type UpdateActionKind, } from '@/lib/updateRuntime'; import { requestMeshTerminalOpen, subscribeSecureMeshTerminalLauncherOpen, } from '@/lib/meshTerminalLauncher'; import { purgeBrowserContactGraph, purgeBrowserSigningMaterial, setSecureModeCached, getNodeIdentity, generateNodeKeys } from '@/mesh/meshIdentity'; import { purgeBrowserDmState } from '@/mesh/meshDmWorkerClient'; import { DEFAULT_INFONET_SEED_URL, fetchInfonetNodeStatusSnapshot, type InfonetNodeStatusSnapshot, } from '@/mesh/controlPlaneStatusClient'; import { fetchWormholeStatus, prepareWormholeInteractiveLane, } from '@/mesh/wormholeIdentityClient'; import { fetchWormholeSettings } from '@/mesh/wormholeClient'; import packageJson from '../../package.json'; type UpdateStatus = | 'idle' | 'checking' | 'available' | 'uptodate' | 'error' | 'confirming' | 'updating' | 'restarting' | 'update_error' | 'docker_update'; const DEFAULT_RELEASES_URL = 'https://github.com/BigBodyCobain/Shadowbroker/releases/latest'; const AUTO_UPDATE_DETAIL = 'This runtime can use the backend-managed update path. Docker deployments will show pull instructions instead of modifying files in place.'; const DESKTOP_UPDATER_DETAIL = 'This packaged desktop app can install the signed update in place. It will restart ShadowBroker after the installer finishes.'; function packagedUpdateDetail(ownsLocalBackend: boolean): string { if (ownsLocalBackend) { return 'This desktop installer updates the app and its bundled local backend together.'; } return 'This packaged desktop app updates through a new installer download. It does not update the separately running backend service.'; } interface TopRightControlsProps { onTerminalToggle?: () => void; onInfonetToggle?: () => void; dmCount?: number; onSettingsClick?: () => void; onMeshChatNavigate?: (tab: 'infonet' | 'meshtastic' | 'dms') => void; } export default function TopRightControls({ onTerminalToggle, onInfonetToggle, dmCount, onMeshChatNavigate, }: TopRightControlsProps = {}) { const [updateStatus, setUpdateStatus] = useState('idle'); const [latestVersion, setLatestVersion] = useState(''); const [errorMessage, setErrorMessage] = useState(''); const [manualUpdateUrl, setManualUpdateUrl] = useState(DEFAULT_RELEASES_URL); const [releasePageUrl, setReleasePageUrl] = useState(DEFAULT_RELEASES_URL); const [dockerCommands, setDockerCommands] = useState(''); const [updateAction, setUpdateAction] = useState('auto_apply'); const [updateDetail, setUpdateDetail] = useState(AUTO_UPDATE_DETAIL); const pollRef = useRef | null>(null); const timeoutRef = useRef | null>(null); const [launcherOpen, setLauncherOpen] = useState(false); const [nodeStep, setNodeStep] = useState<'prompt' | 'terms' | 'activating' | 'disable'>('prompt'); const [activatingPhase, setActivatingPhase] = useState<'keys' | 'peers' | 'sync' | 'done'>('keys'); const activatingPollRef = useRef | null>(null); const activatingTimeoutRef = useRef | null>(null); const [activatingTimedOut, setActivatingTimedOut] = useState(false); const [nodeStatus, setNodeStatus] = useState(null); const [nodeStatusError, setNodeStatusError] = useState(''); const [portalReady, setPortalReady] = useState(false); const [nodeToggleBusy, setNodeToggleBusy] = useState(false); const [nodeToggleError, setNodeToggleError] = useState(''); const [terminalLauncherOpen, setTerminalLauncherOpen] = useState(false); const [terminalLaunchBusy, setTerminalLaunchBusy] = useState(false); const [terminalLaunchError, setTerminalLaunchError] = useState(''); const [terminalPrivateEnabled, setTerminalPrivateEnabled] = useState(false); const [terminalPrivateReady, setTerminalPrivateReady] = useState(false); const [terminalTransportTier, setTerminalTransportTier] = useState('public_degraded'); const currentVersion = packageJson.version; const launchTerminalDirect = () => { if (onTerminalToggle) { onTerminalToggle(); return; } if (onInfonetToggle) { onInfonetToggle(); return; } requestMeshTerminalOpen('top-right-controls'); }; const openTerminalLauncher = useCallback(async () => { setTerminalLaunchError(''); try { const [settings, status] = await Promise.all([ fetchWormholeSettings(true).catch(() => null), fetchWormholeStatus().catch(() => null), ]); const enabled = Boolean(settings?.enabled ?? status?.running ?? status?.ready ?? false); const ready = Boolean(status?.ready); setTerminalPrivateEnabled(enabled); setTerminalPrivateReady(ready); setTerminalTransportTier( String(status?.transport_tier || status?.transport_active || 'public_degraded'), ); } catch (error) { const message = typeof error === 'object' && error !== null && 'message' in error ? String((error as { message?: string }).message || '') : ''; setTerminalPrivateEnabled(false); setTerminalPrivateReady(false); setTerminalTransportTier('public_degraded'); setTerminalLaunchError(message || 'Private-lane status unavailable.'); } setTerminalLauncherOpen(true); }, []); useEffect(() => { return subscribeSecureMeshTerminalLauncherOpen(() => { void openTerminalLauncher(); }); }, [openTerminalLauncher]); const closeTerminalLauncher = () => { if (terminalLaunchBusy) return; setTerminalLauncherOpen(false); setTerminalLaunchError(''); }; const applySecureModeBoundary = async (enabled: boolean) => { setSecureModeCached(enabled); if (!enabled) return; purgeBrowserSigningMaterial(); purgeBrowserContactGraph(); await purgeBrowserDmState(); }; const continueTerminalLaunchInBackground = useCallback(async () => { try { const prepared = await prepareWormholeInteractiveLane({ bootstrapIdentity: true }); const settings = await fetchWormholeSettings(true).catch(() => null); let runtime = await fetchWormholeStatus().catch(() => null); const enabled = Boolean( settings?.enabled ?? prepared.settingsEnabled ?? runtime?.running ?? runtime?.ready ?? false, ); const identityNodeId = String(prepared.identity?.node_id || '').trim(); await applySecureModeBoundary(enabled); runtime = await fetchWormholeStatus().catch(() => runtime); setTerminalPrivateEnabled(enabled); setTerminalPrivateReady(Boolean(runtime?.ready ?? prepared.ready ?? false)); setTerminalTransportTier( String( runtime?.transport_tier || runtime?.transport_active || prepared.transportTier || 'private_control_only', ), ); setTerminalLaunchError(''); setSecureModeCached(enabled); if (identityNodeId) { console.info('[top-right] Wormhole terminal launch ready', identityNodeId); } } catch (error) { const message = typeof error === 'object' && error !== null && 'message' in error ? String((error as { message?: string }).message || '') : ''; const settings = await fetchWormholeSettings(true).catch(() => null); const runtime = await fetchWormholeStatus().catch(() => null); setTerminalPrivateEnabled(Boolean(settings?.enabled ?? runtime?.running ?? runtime?.ready ?? false)); setTerminalPrivateReady(Boolean(runtime?.ready)); setTerminalTransportTier( String(runtime?.transport_tier || runtime?.transport_active || 'public_degraded'), ); setTerminalLaunchError(message || 'Wormhole is still warming up in the background.'); } finally { setTerminalLaunchBusy(false); } }, [applySecureModeBoundary]); const activateWormholeAndLaunchTerminal = async () => { setTerminalLaunchBusy(true); setTerminalLaunchError(''); setTerminalPrivateEnabled(true); setTerminalPrivateReady(false); setTerminalLauncherOpen(false); launchTerminalDirect(); void continueTerminalLaunchInBackground(); }; // Cleanup polling on unmount useEffect(() => { return () => { if (pollRef.current) clearInterval(pollRef.current); if (timeoutRef.current) clearTimeout(timeoutRef.current); if (activatingPollRef.current) clearInterval(activatingPollRef.current); if (activatingTimeoutRef.current) clearTimeout(activatingTimeoutRef.current); }; }, []); const refreshNodeStatus = async () => { const data = await fetchInfonetNodeStatusSnapshot(true); setNodeStatus(data); setNodeStatusError(''); return data; }; const stopActivatingPolls = useCallback(() => { if (activatingPollRef.current) { clearInterval(activatingPollRef.current); activatingPollRef.current = null; } if (activatingTimeoutRef.current) { clearTimeout(activatingTimeoutRef.current); activatingTimeoutRef.current = null; } }, []); const setNodeEnabled = async (enabled: boolean) => { setNodeToggleBusy(true); setNodeToggleError(''); try { // Auto-generate keys on first activation if (enabled) { setActivatingPhase('keys'); setActivatingTimedOut(false); setNodeStep('activating'); const existing = getNodeIdentity(); if (!existing) { await generateNodeKeys(); } setActivatingPhase('peers'); } const res = await controlPlaneFetch('/api/settings/node', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ enabled }), requireAdminSession: false, }); const data = (await res.json().catch(() => ({}))) as { detail?: string; message?: string; }; if (!res.ok) { throw new Error(data?.detail || data?.message || 'Node settings update failed'); } await refreshNodeStatus(); if (enabled) { // Start fast-polling for sync progress setActivatingPhase('sync'); stopActivatingPolls(); activatingPollRef.current = setInterval(async () => { try { const snap = await fetchInfonetNodeStatusSnapshot(true); setNodeStatus(snap); const outcome = String(snap?.sync_runtime?.last_outcome || '').toLowerCase(); if (outcome === 'ok' || outcome === 'solo') { setActivatingPhase('done'); stopActivatingPolls(); // Auto-transition to 'disable' after brief success display setTimeout(() => setNodeStep('disable'), 2500); } } catch { /* ignore poll errors */ } }, 3000); // Timeout after 90s activatingTimeoutRef.current = setTimeout(() => { setActivatingTimedOut(true); }, 90000); } else { // Disabling — close modal setLauncherOpen(false); setNodeStep('prompt'); } } catch (error) { const message = typeof error === 'object' && error !== null && 'message' in error ? String((error as { message?: string }).message || '') : ''; setNodeToggleError(message || 'Node settings update failed'); if (enabled) setNodeStep('terms'); // Go back to terms on error } finally { setNodeToggleBusy(false); } }; useEffect(() => { setPortalReady(true); }, []); useEffect(() => { let alive = true; const fetchWormhole = async () => { try { const data = await fetchWormholeSettings(); const enabled = Boolean(data?.enabled); await applySecureModeBoundary(enabled); } catch { /* ignore */ } }; const fetchNodeStatus = async () => { try { const data = await fetchInfonetNodeStatusSnapshot(true); if (alive) { setNodeStatus(data); setNodeStatusError(''); } } catch (error) { if (!alive) return; const message = typeof error === 'object' && error !== null && 'message' in error ? String((error as { message?: string }).message || '') : ''; setNodeStatusError(message || 'node runtime unavailable'); } }; const poll = () => { fetchWormhole(); fetchNodeStatus(); }; poll(); const interval = setInterval(poll, 15000); return () => { alive = false; clearInterval(interval); }; }, []); const checkForUpdates = async () => { setUpdateStatus('checking'); try { const desktopContext = await getDesktopUpdateContext(); const runtime = classifyUpdateRuntime(desktopContext); const res = await fetch( 'https://api.github.com/repos/BigBodyCobain/Shadowbroker/releases/latest', ); if (!res.ok) throw new Error('Failed to fetch'); const data = (await res.json()) as GitHubLatestRelease; const latest = data.tag_name?.replace('v', '') || data.name?.replace('v', ''); const current = currentVersion.replace('v', ''); const releaseUrl = typeof data.html_url === 'string' && data.html_url.trim().length > 0 ? data.html_url : DEFAULT_RELEASES_URL; const platform = desktopContext?.platform || 'unknown'; const ownsLocalBackend = Boolean(desktopContext?.owns_local_backend); setReleasePageUrl(releaseUrl); setManualUpdateUrl(getPreferredManualUpdateUrl(data, runtime, platform)); let resolvedAction = getUpdateAction(runtime); let resolvedDetail = runtime === 'desktop_packaged' ? packagedUpdateDetail(ownsLocalBackend) : AUTO_UPDATE_DETAIL; if (runtime === 'desktop_packaged') { try { const desktopUpdate = await checkDesktopUpdaterUpdate(); if (desktopUpdate?.version) { resolvedAction = 'desktop_updater'; resolvedDetail = DESKTOP_UPDATER_DETAIL; setLatestVersion(desktopUpdate.version.replace(/^v/i, '')); setUpdateAction(resolvedAction); setUpdateDetail(resolvedDetail); setUpdateStatus('available'); return; } } catch (desktopUpdaterError) { console.warn('Desktop updater check failed; falling back to release download:', desktopUpdaterError); } } setUpdateAction(resolvedAction); setUpdateDetail( resolvedDetail, ); if (latest && latest !== current) { setLatestVersion(latest); setUpdateStatus('available'); } else { setUpdateStatus('uptodate'); setTimeout(() => setUpdateStatus('idle'), 3000); } } catch (err) { console.error('Update check failed:', err); setUpdateStatus('error'); setTimeout(() => setUpdateStatus('idle'), 3000); } }; const startRestartPolling = () => { setUpdateStatus('restarting'); // Poll /api/health until backend comes back pollRef.current = setInterval(async () => { try { const h = await fetch(`${API_BASE}/api/health`); if (h.ok) { if (pollRef.current) clearInterval(pollRef.current); if (timeoutRef.current) clearTimeout(timeoutRef.current); window.location.reload(); } } catch { // Backend still down — keep polling } }, 3000); // Give up after 90 seconds timeoutRef.current = setTimeout(() => { if (pollRef.current) clearInterval(pollRef.current); setErrorMessage('Restart timed out — the app may need to be started manually.'); setUpdateStatus('update_error'); }, 90000); }; const triggerUpdate = async () => { if (updateAction === 'manual_download') { window.open(manualUpdateUrl, '_blank', 'noopener,noreferrer'); setUpdateStatus('idle'); return; } if (updateAction === 'desktop_updater') { setUpdateStatus('updating'); setErrorMessage(''); try { await installDesktopUpdaterUpdate(); setUpdateStatus('restarting'); } catch (err) { const message = typeof err === 'object' && err !== null && 'message' in err ? String((err as { message?: string }).message) : ''; setErrorMessage( message === 'desktop_update_installed_restart_required' ? 'Update installed. Restart ShadowBroker to finish applying it.' : message || 'Desktop updater failed. Use manual download if this keeps happening.', ); setUpdateStatus('update_error'); } return; } setUpdateStatus('updating'); setErrorMessage(''); try { const res = await controlPlaneFetch('/api/system/update', { method: 'POST' }); const data = (await res.json().catch(() => ({}))) as { ok?: boolean; status?: string; message?: string; detail?: string; manual_url?: string; release_url?: string; docker_commands?: string; }; if (typeof data.manual_url === 'string' && data.manual_url.trim().length > 0) { setManualUpdateUrl(data.manual_url); } if (typeof data.release_url === 'string' && data.release_url.trim().length > 0) { setReleasePageUrl(data.release_url); } if (data?.status === 'docker') { setDockerCommands(data.docker_commands || 'docker compose pull && docker compose up -d'); setUpdateStatus('docker_update'); return; } if (data?.status === 'manual') { const targetUrl = typeof data.manual_url === 'string' && data.manual_url.trim().length > 0 ? data.manual_url : manualUpdateUrl; window.open(targetUrl, '_blank', 'noopener,noreferrer'); setUpdateStatus('idle'); return; } if (!res.ok || data?.ok === false || data?.status === 'error') { const message = data?.detail || data?.message || 'control_plane_request_failed'; const error = new Error(message) as Error & { manualUrl?: string }; error.manualUrl = data?.manual_url; throw error; } startRestartPolling(); } catch (err) { // The update extracts files over the project, which causes the Next.js // dev server to hot-reload and drop the proxy connection mid-request. // A network error during update likely means it SUCCEEDED and the // server is restarting — transition to polling instead of showing failure. const message = typeof err === 'object' && err !== null && 'message' in err ? String((err as { message?: string }).message) : ''; const isNetworkDrop = err instanceof TypeError || message === 'Failed to fetch'; if (isNetworkDrop) { startRestartPolling(); } else { const manualUrl = typeof err === 'object' && err !== null && 'manualUrl' in err ? String((err as { manualUrl?: string }).manualUrl || '') : ''; if (manualUrl) { setManualUpdateUrl(manualUrl); } setErrorMessage(message || 'Unknown error'); setUpdateStatus('update_error'); } } }; // ── Confirmation Dialog ── const renderConfirmDialog = () => (
{/* Header */}
UPDATE v{currentVersion} → v{latestVersion}
{/* Actions */}

{updateDetail}

{updateAction === 'manual_download' ? 'VIEW RELEASE' : 'MANUAL DOWNLOAD'}
); // ── Error Dialog ── const renderErrorDialog = () => (
UPDATE FAILED

{errorMessage}

{updateAction === 'manual_download' ? 'VIEW RELEASE' : 'MANUAL DOWNLOAD'}
); // ── Docker Update Dialog ── const renderDockerDialog = () => (
DOCKER UPDATE — v{latestVersion}

Docker containers must be updated by pulling new images. Run this on your host machine:

{dockerCommands}
VIEW RELEASE
); const nodeMode = String(nodeStatus?.node_mode || 'participant').trim().toUpperCase(); const nodeEnabled = Boolean(nodeStatus?.node_enabled); const syncOutcomeRaw = String(nodeStatus?.sync_runtime?.last_outcome || 'idle') .trim() .toLowerCase(); const syncError = String(nodeStatus?.sync_runtime?.last_error || '').trim().toLowerCase(); const syncOutcome = !nodeEnabled ? 'OFF' : syncOutcomeRaw === 'solo' || syncError === 'no active sync peers' ? 'SOLO' : syncOutcomeRaw === 'ok' ? 'CONNECTED' : syncOutcomeRaw === 'running' ? 'SYNCING' : syncOutcomeRaw === 'fork' ? 'FORK STOP' : syncOutcomeRaw === 'error' ? 'SYNC ISSUE' : 'ACTIVE'; const bootstrapFailed = Boolean(nodeStatus?.bootstrap?.last_bootstrap_error); const nodeIndicatorClass = !nodeEnabled ? 'bg-rose-400' : syncOutcomeRaw === 'solo' || syncError === 'no active sync peers' ? 'bg-cyan-400' : syncOutcomeRaw === 'ok' ? 'bg-green-400' : syncOutcomeRaw === 'fork' || bootstrapFailed ? 'bg-amber-400' : syncOutcomeRaw === 'error' ? 'bg-rose-400' : 'bg-cyan-400'; const nodeTitle = !nodeEnabled ? `${nodeMode} node • off` : bootstrapFailed ? `${nodeMode} node • bootstrap warning` : `${nodeMode} node • ${syncOutcome.toLowerCase()}`; const closeLauncher = () => { stopActivatingPolls(); setLauncherOpen(false); setNodeStep('prompt'); setNodeToggleError(''); setActivatingTimedOut(false); }; // Uniform button style (matches UPDATES button) const btnBase = 'flex items-center justify-center gap-1 px-2 py-1.5 bg-[var(--bg-primary)]/70 border border-[var(--border-primary)] hover:border-cyan-500/50 hover:bg-[var(--hover-accent)] transition-all text-[10px] text-[var(--text-secondary)] font-mono cursor-pointer flex-1'; const nodeLauncherModal = portalReady && launcherOpen ? createPortal(
{nodeStep === 'disable' ? ( <>
Node activated. {(() => { const id = getNodeIdentity(); return id?.nodeId ? (
{id.nodeId}
) : null; })()}
{syncOutcome.toLowerCase()} {(nodeStatus?.total_events ?? 0) > 0 && {nodeStatus?.total_events} events} {(nodeStatus?.bootstrap?.sync_peer_count ?? 0) > 0 && {nodeStatus?.bootstrap?.sync_peer_count} peers}
Your node keeps syncing as long as the backend is running — you can close this browser tab. To run a headless node without the dashboard, use meshnode.bat (Windows) or meshnode.sh (macOS/Linux).
{nodeToggleError && (
{nodeToggleError}
)}
) : nodeStep === 'activating' ? ( <>
{/* Step: Generate identity */}
{activatingPhase === 'keys' ? ( ) : ( )} {activatingPhase === 'keys' ? 'Generating identity...' : 'Identity ready'} {activatingPhase !== 'keys' && (() => { const id = getNodeIdentity(); return id?.nodeId ? ( {id.nodeId} ) : null; })()}
{/* Step: Connect to relay */}
{activatingPhase === 'keys' ? ( ) : activatingPhase === 'peers' ? ( ) : ( )} {activatingPhase === 'keys' ? 'Connecting to default seed...' : activatingPhase === 'peers' ? 'Connecting to default seed...' : 'Default seed connected'}
{/* Step: Sync chain */}
{(activatingPhase === 'keys' || activatingPhase === 'peers') ? ( ) : activatingPhase === 'sync' ? ( ) : ( )} {activatingPhase === 'done' ? (syncOutcomeRaw === 'solo' ? `Solo node ready — ${nodeStatus?.total_events ?? 0} events` : `Synced — ${nodeStatus?.total_events ?? 0} events`) : activatingPhase === 'sync' ? `Syncing chain...${(nodeStatus?.total_events ?? 0) > 0 ? ` ${nodeStatus?.total_events} events` : ''}` : 'Syncing chain...'}
{/* Done banner */} {activatingPhase === 'done' && ( <>
NODE ONLINE
Your node keeps syncing as long as the backend is running — you can close this browser tab. To run a headless node without the dashboard, use meshnode.bat (Windows) or meshnode.sh (macOS/Linux).
)}
{activatingTimedOut && activatingPhase !== 'done' && (
Sync is taking longer than expected. Your node is active and will continue syncing in the background.
)} {nodeToggleError && (
{nodeToggleError}
)} {(activatingTimedOut || activatingPhase === 'done') && ( )} ) : nodeStep === 'prompt' ? ( <>
Do you want to activate a node on this install?
This turns on your local participant node and lets this install keep syncing the public Infonet chain from {DEFAULT_INFONET_SEED_URL}.
{(bootstrapFailed || nodeStatusError || nodeToggleError) && (
{nodeToggleError || nodeStatusError || nodeStatus?.bootstrap?.last_bootstrap_error || 'Node runtime warning detected.'}
)}
) : ( <>
BY CONTINUING YOU AGREE:
  • This install can keep a local copy of the public Infonet chain.
  • Fresh installs pull from the bundled default seed at {DEFAULT_INFONET_SEED_URL}.
  • Participant-node sync is public-facing unless you separately use obfuscated-lane features.
  • Your backend may sync with configured or bundled bootstrap peers in the background.
  • Wormhole provides gates (transitional private lane) and Dead Drop / DM (stronger private lane) as separate postures.
{nodeMode} • {syncOutcome}
)}
, document.body, ) : null; const terminalStatusLabel = terminalPrivateReady ? 'PRIVATE LANE READY' : terminalPrivateEnabled ? 'PRIVATE LANE STARTING' : 'PRIVATE LANE OFFLINE'; const terminalStatusTone = terminalPrivateReady ? 'text-emerald-300' : terminalPrivateEnabled ? 'text-amber-300' : 'text-cyan-300'; const terminalLauncherModal = portalReady && terminalLauncherOpen ? createPortal(
{terminalPrivateReady ? 'Enter the Wormhole-facing terminal and sync with the obfuscated Infonet commons?' : 'The terminal runs through Wormhole for obfuscated gates, inbox, and experimental comms.'}
{terminalPrivateReady ? 'Your obfuscated identity is already provisioned. Entering now keeps the obfuscated lane separate from the public node sync path.' : 'This turns Wormhole on and opens the obfuscated lane. If you already have a Wormhole identity, it reuses it. If you do not, it bootstraps one once and then keeps using it.'}
{terminalLaunchError && (
{terminalLaunchError}
)}
BEFORE YOU ENTER:
  • The terminal is for Wormhole gates (transitional private lane) and Dead Drop / DM (stronger private lane).
  • Your participant node can stay active separately without changing this obfuscated identity lane.
  • Mesh remains the public perimeter. Wormhole is the obfuscated commons.
WORMHOLE CLEANUP:
Closing the Infonet terminal will shut down Wormhole automatically. If you force-close the browser or the shutdown fails, Wormhole may keep running in the background. Run killwormhole.bat (Windows) or{' '} killwormhole.sh (macOS/Linux) from the project root to ensure it is fully stopped.
, document.body, ) : null; return ( <> {terminalLauncherModal} {nodeLauncherModal}
{/* Node runtime / private lane */} {/* Terminal toggle */} {/* ── Update Available → opens confirmation ── */} {updateStatus === 'available' && ( )} {/* ── Confirming → show dialog ── */} {updateStatus === 'confirming' && ( <> {renderConfirmDialog()} )} {/* ── Updating → spinner ── */} {updateStatus === 'updating' && (
DOWNLOADING UPDATE...
)} {/* ── Restarting → spinner + waiting ── */} {updateStatus === 'restarting' && (
RESTARTING...
)} {/* ── Error → show error dialog ── */} {updateStatus === 'update_error' && ( <> {renderErrorDialog()} )} {/* ── Docker update → show pull instructions ── */} {updateStatus === 'docker_update' && ( <> {renderDockerDialog()} )} {/* ── Default states: idle / checking / uptodate / check-error ── */} {!['available', 'confirming', 'updating', 'restarting', 'update_error', 'docker_update'].includes( updateStatus, ) && ( )}
); }