'use client'; import { useEffect, useState, useRef, useCallback, useMemo } from 'react'; import dynamic from 'next/dynamic'; import { motion } from 'framer-motion'; import { ChevronLeft, ChevronRight, ChevronUp, ChevronDown } from 'lucide-react'; import WorldviewLeftPanel from '@/components/WorldviewLeftPanel'; import NewsFeed from '@/components/NewsFeed'; import MarketsPanel from '@/components/MarketsPanel'; import FilterPanel from '@/components/FilterPanel'; import FindLocateBar from '@/components/FindLocateBar'; import TopRightControls from '@/components/TopRightControls'; import TimelinePanel from '@/components/TimelinePanel'; import SettingsPanel from '@/components/SettingsPanel'; import MapLegend from '@/components/MapLegend'; import ScaleBar from '@/components/ScaleBar'; import MeshTerminal from '@/components/MeshTerminal'; import MeshChat from '@/components/MeshChat'; import InfonetTerminal from '@/components/InfonetTerminal'; import { endInfonetTerminalSession } from '@/lib/infonetTerminalSession'; import ShodanPanel from '@/components/ShodanPanel'; import ReconPanel from '@/components/ReconPanel'; import ScmPanel from '@/components/ScmPanel'; import EntityGraphPanel from '@/components/EntityGraphPanel'; import { isEntityGraphEligible } from '@/lib/entityGraph'; import AIIntelPanel from '@/components/AIIntelPanel'; import GlobalTicker from '@/components/GlobalTicker'; import ErrorBoundary from '@/components/ErrorBoundary'; import OnboardingModal, { useOnboarding } from '@/components/OnboardingModal'; import ChangelogModal, { useChangelog } from '@/components/ChangelogModal'; import StartupWarmupModal, { useStartupWarmupNotice } from '@/components/StartupWarmupModal'; import type { ActiveLayers, KiwiSDR, Scanner, SelectedEntity } from '@/types/dashboard'; import type { ShodanSearchMatch } from '@/types/shodan'; import { API_BASE } from '@/lib/api'; import { useDataPolling, LAYER_TOGGLE_EVENT } from '@/hooks/useDataPolling'; import { useBackendStatus, useDataKey, useDataKeys } from '@/hooks/useDataStore'; import { useReverseGeocode } from '@/hooks/useReverseGeocode'; import { useRegionDossier } from '@/hooks/useRegionDossier'; import { useGtDossier } from '@/hooks/useGtDossier'; import { useAgentActions } from '@/hooks/useAgentActions'; import { useFeedHealth } from '@/hooks/useFeedHealth'; import { useKeyboardShortcuts } from '@/hooks/useKeyboardShortcuts'; import KeyboardShortcutsOverlay from '@/components/KeyboardShortcutsOverlay'; import AlertToast from '@/components/AlertToast'; import AisUpstreamBanner from '@/components/AisUpstreamBanner'; import { useAlertToasts } from '@/hooks/useAlertToasts'; import { useWatchlist } from '@/hooks/useWatchlist'; import WatchlistWidget from '@/components/WatchlistWidget'; import { requestSecureMeshTerminalLauncherOpen, subscribeMeshTerminalOpen, } from '@/lib/meshTerminalLauncher'; import { hasSentinelInfoBeenSeen, markSentinelInfoSeen, hasSentinelCredentials, checkBackendSentinelStatus, } from '@/lib/sentinelHub'; import { useTranslation } from '@/i18n'; import { LocateBar } from './LocateBar'; import { SentinelInfoModal } from './SentinelInfoModal'; import SarAoiEditorModal from '@/components/SarAoiEditorModal'; // Use dynamic loads for Maplibre to avoid SSR window is not defined errors const MaplibreViewer = dynamic(() => import('@/components/MaplibreViewer'), { ssr: false }); // LocateBar and SentinelInfoModal extracted to page-local modules (Sprint 4B) export default function Dashboard() { const viewBoundsRef = useRef<{ south: number; west: number; north: number; east: number } | null>(null); const { t } = useTranslation(); // Start the critical map data request before panel/control-plane effects. // Non-map widgets can warm up after this; first paint needs flights, ships, and intel first. useDataPolling(); const { mouseCoords, locationLabel, handleMouseCoords } = useReverseGeocode(); const [selectedEntity, setSelectedEntity] = useState(null); const [showEntityGraph, setShowEntityGraph] = useState(false); useEffect(() => { setShowEntityGraph(false); }, [selectedEntity]); const [trackedSdr, setTrackedSdr] = useState(null); const [trackedScanner, setTrackedScanner] = useState(null); const { regionDossier, regionDossierLoading, handleMapRightClick } = useRegionDossier( selectedEntity, setSelectedEntity, ); // Agent can push satellite imagery to the same full-screen viewer as right-click, // and can fly the map to a point (e.g. sar_focus_aoi). The hook is invoked // below — after setFlyToLocation is declared — so the fly_to callback can // close over it without hitting a temporal dead zone. const [uiVisible, setUiVisible] = useState(true); const [leftOpen, setLeftOpen] = useState(true); const [rightOpen, setRightOpen] = useState(true); const [tickerOpen, setTickerOpen] = useState(true); // Persist UI panel states useEffect(() => { const l = localStorage.getItem('sb_left_open'); const r = localStorage.getItem('sb_right_open'); const tk = localStorage.getItem('sb_ticker_open'); if (l !== null) setLeftOpen(l === 'true'); if (r !== null) setRightOpen(r === 'true'); if (tk !== null) setTickerOpen(tk === 'true'); }, []); useEffect(() => { localStorage.setItem('sb_left_open', leftOpen.toString()); }, [leftOpen]); useEffect(() => { localStorage.setItem('sb_right_open', rightOpen.toString()); }, [rightOpen]); useEffect(() => { localStorage.setItem('sb_ticker_open', tickerOpen.toString()); }, [tickerOpen]); // Issue #298: kick the one-time backend Sentinel-status check on mount. // This populates the cached value that ``hasSentinelCredentials()`` reads // synchronously elsewhere (MaplibreViewer's tile-URL memo, the // Sentinel-info modal flow). Fire-and-forget — the cache stays false // until resolved so the UI fails safely. useEffect(() => { void checkBackendSentinelStatus(); }, []); const [settingsOpen, setSettingsOpen] = useState(false); const [legendOpen, setLegendOpen] = useState(false); const [shortcutsOpen, setShortcutsOpen] = useState(false); const [terminalOpen, setTerminalOpen] = useState(false); const [terminalLaunchToken, setTerminalLaunchToken] = useState(0); const [infonetOpen, setInfonetOpen] = useState(false); const [meshChatLaunchRequest, setMeshChatLaunchRequest] = useState<{ tab: 'infonet' | 'meshtastic' | 'dms'; gate?: string; peerId?: string; showSas?: boolean; nonce: number; } | null>(null); const [dmCount, setDmCount] = useState(0); const [mapView, setMapView] = useState({ zoom: 2, latitude: 20 }); const [locateBarOpen, setLocateBarOpen] = useState(false); const [measureMode, setMeasureMode] = useState(false); const [measurePoints, setMeasurePoints] = useState<{ lat: number; lng: number }[]>([]); const [pinPlacementMode, setPinPlacementMode] = useState(false); // SAR AOI editor + map drop mode const [sarAoiEditorOpen, setSarAoiEditorOpen] = useState(false); const [sarAoiDropMode, setSarAoiDropMode] = useState(false); const [sarAoiDroppedCoords, setSarAoiDroppedCoords] = useState<{ lat: number; lng: number } | null>(null); const sarAoiListChangedRef = useRef(0); const [sarAoiListVersion, setSarAoiListVersion] = useState(0); const openMeshTerminal = useCallback(() => { setTerminalOpen(true); setTerminalLaunchToken((prev) => prev + 1); }, []); const openInfonet = useCallback(() => { setInfonetOpen(true); }, []); const openSecureTerminalLauncher = useCallback(() => { requestSecureMeshTerminalLauncherOpen('dashboard'); }, []); useEffect(() => subscribeMeshTerminalOpen(openInfonet), [openInfonet]); const toggleInfonet = useCallback(() => { setInfonetOpen((prev) => { if (prev) { void endInfonetTerminalSession(); return false; } return true; }); }, []); const [activeLayers, setActiveLayers] = useState({ // Aircraft — all ON flights: true, private: true, jets: true, military: true, tracked: true, gps_jamming: true, // Maritime — all ON ships_military: true, ships_cargo: true, ships_civilian: true, ships_passenger: true, ships_tracked_yachts: true, fishing_activity: true, // Space — only satellites satellites: true, gibs_imagery: false, highres_satellite: false, sentinel_hub: false, viirs_nightlights: false, road_corridor_trends: false, malware_c2: false, submarine_cables: false, scm_suppliers: false, cyber_threats: false, telegram_osint: true, // Hazards — no fire, rest ON earthquakes: true, firms: false, ukraine_alerts: true, weather_alerts: true, volcanoes: true, air_quality: true, // Infrastructure — military bases + internet outages only cctv: false, datacenters: false, internet_outages: true, power_plants: false, military_bases: true, trains: false, // SIGINT — all ON except HF digital spots kiwisdr: true, psk_reporter: false, satnogs: true, tinygs: true, scanners: true, sigint_meshtastic: true, sigint_aprs: true, // Overlays ukraine_frontline: true, global_incidents: true, day_night: true, correlations: true, contradictions: true, uap_sightings: true, // Biosurveillance wastewater: true, // CrowdThreat is operator opt-in only. crowdthreat: false, gt_risk: false, // Shodan shodan_overlay: false, // AI Intel ai_intel: true, // SAR (Synthetic Aperture Radar) sar: true, }); const regionLat = selectedEntity?.type === 'region_dossier' ? selectedEntity.extra?.lat : undefined; const regionLng = selectedEntity?.type === 'region_dossier' ? selectedEntity.extra?.lng : undefined; const { gtDossier, gtDossierLoading } = useGtDossier( typeof regionLat === 'number' ? regionLat : undefined, typeof regionLng === 'number' ? regionLng : undefined, regionDossier?.country?.name, activeLayers.gt_risk, ); const [shodanResults, setShodanResults] = useState([]); const [, setShodanQueryLabel] = useState(''); const [shodanStyle, setShodanStyle] = useState({ shape: 'circle', color: '#16a34a', size: 'md' }); const backendStatus = useBackendStatus(); const spaceWeather = useDataKey('space_weather'); const feedHealth = useFeedHealth(); const bootSignals = useDataKeys([ 'bootstrap_ready', 'commercial_flights', 'military_flights', 'tracked_flights', 'ships', 'news', 'threat_level', ] as const); const criticalPaintReady = Boolean( bootSignals.bootstrap_ready || (bootSignals.commercial_flights?.length || 0) > 0 || (bootSignals.military_flights?.length || 0) > 0 || (bootSignals.tracked_flights?.length || 0) > 0 || (bootSignals.ships?.length || 0) > 0 || (bootSignals.news?.length || 0) > 0 || bootSignals.threat_level, ); const [secondaryBootReady, setSecondaryBootReady] = useState(false); useEffect(() => { if (secondaryBootReady) return; const delay = criticalPaintReady ? 900 : 5500; const id = window.setTimeout(() => setSecondaryBootReady(true), delay); return () => window.clearTimeout(id); }, [criticalPaintReady, secondaryBootReady]); // Global keyboard shortcuts useKeyboardShortcuts({ toggleLeft: () => setLeftOpen((p) => !p), toggleRight: () => setRightOpen((p) => !p), toggleMarkets: () => setTickerOpen((p) => !p), openSettings: () => setSettingsOpen(true), openLegend: () => setLegendOpen((p) => !p), openShortcuts: () => setShortcutsOpen((p) => !p), deselectEntity: () => { if (shortcutsOpen) { setShortcutsOpen(false); return; } if (settingsOpen) { setSettingsOpen(false); return; } if (legendOpen) { setLegendOpen(false); return; } setSelectedEntity(null); }, focusSearch: () => { const el = document.querySelector('[data-search-input]'); el?.focus(); }, }); // Alert toast notifications for high-severity news const { toasts, dismiss: dismissToast } = useAlertToasts(); // Persistent entity watchlist const { items: watchlistItems, removeFromWatchlist, clearWatchlist } = useWatchlist(); // Notify backend of layer toggles so it can skip disabled fetchers / stop streams. // After the POST completes, dispatch a custom event so useDataPolling immediately // refetches slow-tier data — this makes toggled layers (power plants, GDELT, etc.) // appear instantly instead of waiting up to 120 seconds. const layersTimerRef = useRef | null>(null); const initialLayerSyncRef = useRef(false); useEffect(() => { if (!secondaryBootReady) return; const syncLayers = (triggerRefetch: boolean) => fetch(`${API_BASE}/api/layers`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ layers: activeLayers }), }).then(() => { if (triggerRefetch) { window.dispatchEvent(new Event(LAYER_TOGGLE_EVENT)); } }).catch((e) => console.warn('Backend layer sync will retry after runtime is reachable:', e)); if (layersTimerRef.current) clearTimeout(layersTimerRef.current); if (!initialLayerSyncRef.current) { initialLayerSyncRef.current = true; void syncLayers(false); } else { layersTimerRef.current = setTimeout(() => { void syncLayers(true); }, 250); } return () => { if (layersTimerRef.current) clearTimeout(layersTimerRef.current); }; }, [activeLayers, secondaryBootReady]); // Left panel accordion state const [leftDataMinimized, setLeftDataMinimized] = useState(false); const [leftMeshExpanded, setLeftMeshExpanded] = useState(true); const [leftShodanMinimized, setLeftShodanMinimized] = useState(true); const launchMeshChatTab = useCallback( ( tab: 'infonet' | 'meshtastic' | 'dms', gate?: string, peerId?: string, showSas?: boolean, ) => { setLeftOpen(true); setLeftMeshExpanded(true); setMeshChatLaunchRequest({ tab, gate, peerId, showSas, nonce: Date.now() }); }, [], ); const openLiveGateFromShell = useCallback((gate: string) => { setInfonetOpen(false); launchMeshChatTab('infonet', gate); }, [launchMeshChatTab]); const openDeadDropFromShell = useCallback( (peerId: string, options?: { showSas?: boolean }) => { setInfonetOpen(false); launchMeshChatTab('dms', undefined, peerId, Boolean(options?.showSas)); }, [launchMeshChatTab], ); // Right panel: which panel is "focused" (expanded). null = none focused, all normal. const [rightFocusedPanel, setRightFocusedPanel] = useState(null); // Auto-expand Data Layers when user starts tracking an SDR/Scanner useEffect(() => { if (trackedSdr || trackedScanner) { setLeftDataMinimized(false); setLeftOpen(true); } }, [trackedSdr, trackedScanner]); // NASA GIBS satellite imagery state const [gibsDate, setGibsDate] = useState(() => { const d = new Date(); d.setDate(d.getDate() - 1); return d.toISOString().slice(0, 10); }); const [gibsOpacity, setGibsOpacity] = useState(0.6); // Sentinel Hub satellite imagery state (user-provided Copernicus CDSE credentials) const [sentinelDate, setSentinelDate] = useState(() => { const d = new Date(); d.setDate(d.getDate() - 5); // Sentinel-2 has ~5-day revisit return d.toISOString().slice(0, 10); }); const [sentinelOpacity, setSentinelOpacity] = useState(0.6); const [sentinelPreset, setSentinelPreset] = useState('TRUE-COLOR'); const [showSentinelInfo, setShowSentinelInfo] = useState(false); const prevSentinelRef = useRef(false); // Show info modal the first time sentinel_hub is toggled on useEffect(() => { if (activeLayers.sentinel_hub && !prevSentinelRef.current) { if (!hasSentinelInfoBeenSeen()) { setShowSentinelInfo(true); markSentinelInfoSeen(); } if (!hasSentinelCredentials()) { // No creds — open settings instead setSettingsOpen(true); } } prevSentinelRef.current = activeLayers.sentinel_hub; }, [activeLayers.sentinel_hub]); const [effects] = useState({ bloom: true, }); const [activeStyle, setActiveStyle] = useState('DEFAULT'); const memoizedEffects = useMemo( () => ({ ...effects, bloom: effects.bloom && activeStyle !== 'DEFAULT', style: activeStyle }), [effects, activeStyle], ); const [flyToLocation, setFlyToLocation] = useState<{ lat: number; lng: number; ts: number; } | null>(null); const handleFlyTo = useCallback( (lat: number, lng: number) => setFlyToLocation({ lat, lng, ts: Date.now() }), [], ); const handleMeasureClick = useCallback( (pt: { lat: number; lng: number }) => { setMeasurePoints((prev) => (prev.length >= 3 ? prev : [...prev, pt])); }, [], ); const stylesList = ['DEFAULT', 'SATELLITE']; const cycleStyle = () => { setActiveStyle((prev) => { const idx = stylesList.indexOf(prev); const next = stylesList[(idx + 1) % stylesList.length]; // Auto-toggle High-Res Satellite layer with SATELLITE style setActiveLayers((l) => ({ ...l, highres_satellite: next === 'SATELLITE' })); return next; }); }; const [activeFilters, setActiveFilters] = useState>({}); const firstPaintActiveLayers = useMemo(() => { if (secondaryBootReady) return activeLayers; return { ...activeLayers, cctv: false, sar: false, gibs_imagery: false, highres_satellite: false, sentinel_hub: false, viirs_nightlights: false, road_corridor_trends: false, psk_reporter: false, tinygs: false, datacenters: false, power_plants: false, }; }, [activeLayers, secondaryBootReady]); // Agent fly_to handler (sar_focus_aoi etc.) — wired here now that // setFlyToLocation is in scope. show_image is routed through // useAgentActions at the top of Dashboard. useAgentActions(handleMapRightClick, ({ lat, lng }) => { setFlyToLocation({ lat, lng, ts: Date.now() }); }, secondaryBootReady); // Eavesdrop Mode State const [isEavesdropping] = useState(false); const [, setEavesdropLocation] = useState<{ lat: number; lng: number } | null>(null); const [, setCameraCenter] = useState<{ lat: number; lng: number } | null>(null); // Onboarding & connection status const { showOnboarding, setShowOnboarding } = useOnboarding(); const { showWarmupNotice, setShowWarmupNotice } = useStartupWarmupNotice(); const { showChangelog, setShowChangelog } = useChangelog(); return ( <>
{/* MAPLIBRE WEBGL OVERLAY */} setPinPlacementMode(false)} sarAoiDropMode={sarAoiDropMode} onSarAoiDropped={(coords) => { setSarAoiDropMode(false); setSarAoiDroppedCoords(coords); setSarAoiEditorOpen(true); }} sarAoiListVersion={sarAoiListVersion} /> {uiVisible && ( <> {/* WORLDVIEW HEADER */}
{/* Target Reticle Icon */}

S H A D O W B R O K E R

{t('brand.subtitle')}
{/* SYSTEM METRICS TOP LEFT */}
{t('brand.systemMetrics')}
{/* SYSTEM METRICS TOP RIGHT — removed, label moved into TimelineScrubber */} {/* LEFT HUD CONTAINER — mirrors right side: one scroll container, scrollbar on LEFT edge */} {/* 1. DATA LAYERS (Top) */}
{secondaryBootReady ? ( setSettingsOpen(true)} onLegendClick={() => setLegendOpen(true)} onOpenSarAoiEditor={() => setSarAoiEditorOpen(true)} gibsDate={gibsDate} setGibsDate={setGibsDate} gibsOpacity={gibsOpacity} setGibsOpacity={setGibsOpacity} sentinelDate={sentinelDate} setSentinelDate={setSentinelDate} sentinelOpacity={sentinelOpacity} setSentinelOpacity={setSentinelOpacity} sentinelPreset={sentinelPreset} setSentinelPreset={setSentinelPreset} onEntityClick={setSelectedEntity} onFlyTo={handleFlyTo} trackedSdr={trackedSdr} setTrackedSdr={setTrackedSdr} trackedScanner={trackedScanner} setTrackedScanner={setTrackedScanner} isMinimized={leftDataMinimized} onMinimizedChange={setLeftDataMinimized} viewBoundsRef={viewBoundsRef} /> ) : (
{t('nav.dataLayers')}
{t('nav.prioritizingMapFeeds')}
)}
{/* 2. MESHTASTIC CHAT (Middle) */} {secondaryBootReady && (
setSettingsOpen(true)} onTerminalToggle={openSecureTerminalLauncher} onOpenLiveGate={openLiveGateFromShell} onOpenDeadDrop={openDeadDropFromShell} launchRequest={meshChatLaunchRequest} />
)} {/* 3. SHODAN CONNECTOR (Bottom) */} {secondaryBootReady && (
setSettingsOpen(true)} settingsOpen={settingsOpen} onResultsChange={(results, queryLabel) => { setShodanResults(results); setShodanQueryLabel(queryLabel); setActiveLayers((prev) => ({ ...prev, shodan_overlay: results.length > 0 })); }} onSelectEntity={setSelectedEntity} onStyleChange={setShodanStyle} isMinimized={leftShodanMinimized} onMinimizedChange={setLeftShodanMinimized} />
)} {/* 4. RECON + SCM */} {secondaryBootReady && (
)} {/* 5. AI INTEL */} {secondaryBootReady && (
)}
{/* LEFT SIDEBAR TOGGLE TAB — aligns with Data Layers section */} {/* RIGHT SIDEBAR TOGGLE TAB */} {/* RIGHT HUD CONTAINER — slides off right edge when hidden */} setSettingsOpen(true)} onMeshChatNavigate={launchMeshChatTab} dmCount={dmCount} /> {/* FIND / LOCATE */}
{ setFlyToLocation({ lat, lng, ts: Date.now() }); }} onFilter={(filterKey, value) => { setActiveFilters((prev) => { const current = prev[filterKey] || []; if (!current.includes(value)) { return { ...prev, [filterKey]: [...current, value] }; } return prev; }); }} />
{/* GLOBAL TICKER REPLACES MARKETS PANEL - RENDERED OUTSIDE THIS DIV */} {/* EVENT TIMELINE */} {secondaryBootReady && (
)} {/* DATA FILTERS */}
{/* BOTTOM RIGHT - NEWS FEED (fills remaining space) */}
{ if (isEntityGraphEligible(selectedEntity)) setShowEntityGraph(true); }} onArticleClick={(idx, lat, lng, title) => { if (lat !== undefined && lng !== undefined) { setFlyToLocation({ lat, lng, ts: Date.now() }); // Also highlight the corresponding map alert if (title) { const alertKey = `${title}|${lat},${lng}`; setSelectedEntity({ id: alertKey, type: 'news' }); } } }} />
{/* BOTTOM CENTER COORDINATE / LOCATION BAR — hidden when fullscreen overlays are open */} {!(selectedEntity?.type === 'region_dossier' && regionDossier?.sentinel2) && selectedEntity?.type !== 'cctv' && selectedEntity?.type !== 'news' && ( {/* LOCATE BAR — search by coordinates or place name */} setFlyToLocation({ lat, lng, ts: Date.now() })} onOpenChange={setLocateBarOpen} />
{/* Coordinates */}
{t('controls.coordinates')}
{mouseCoords ? `${mouseCoords.lat.toFixed(4)}, ${mouseCoords.lng.toFixed(4)}` : '0.0000, 0.0000'}
{/* Divider */}
{/* Location name */}
{t('controls.location')}
{locationLabel || t('controls.hoverMap')}
{/* Divider */}
{/* Style preset (compact) */}
{t('controls.style')}
{activeStyle}
{/* Divider */}
{/* Space Weather */} {(() => { const sw = spaceWeather as { kp_index?: number; kp_text?: string } | undefined; return (
{t('controls.solar')}
= 5 ? 'text-red-400' : (sw?.kp_index ?? 0) >= 4 ? 'text-yellow-400' : 'text-green-400' }`} > {sw?.kp_text || t('controls.na')}
); })()} {/* Divider */}
{/* Feed Health */}
{feedHealth.map((f) => (
{f.label} {f.count}
))}
)} )} {/* RESTORE UI BUTTON (If Hidden) */} {!uiVisible && ( )} {/* DYNAMIC SCALE BAR — hidden when fullscreen overlays or locate bar are open */} {!(selectedEntity?.type === 'region_dossier' && regionDossier?.sentinel2) && selectedEntity?.type !== 'cctv' && selectedEntity?.type !== 'news' && !locateBarOpen && (
{ setMeasureMode((m) => !m); if (measureMode) setMeasurePoints([]); }} onClearMeasure={() => setMeasurePoints([])} />
)} {/* STATIC CRT VIGNETTE */}
{/* SCANLINES OVERLAY */}
{/* WATCHLIST WIDGET */} {/* SETTINGS PANEL */} setSettingsOpen(false)} /> {/* MAP LEGEND */} setLegendOpen(false)} /> {/* KEYBOARD SHORTCUTS OVERLAY */} setShortcutsOpen(false)} /> {/* ALERT TOAST NOTIFICATIONS */} {/* AIS UPSTREAM OUTAGE BANNER — renders only when AIS is configured but the WebSocket upstream is unreachable. Tells users the empty ocean isn't their fault. */} {/* ONBOARDING MODAL */} {showOnboarding && ( setShowOnboarding(false)} onOpenSettings={() => { setShowOnboarding(false); setSettingsOpen(true); }} /> )} {/* FIRST-RUN WARMUP NOTICE — shows once after onboarding */} {!showOnboarding && showWarmupNotice && ( setShowWarmupNotice(false)} /> )} {/* v0.4 CHANGELOG MODAL — shows once per version after onboarding */} {!showOnboarding && !showWarmupNotice && showChangelog && ( setShowChangelog(false)} /> )} {/* SENTINEL HUB — first-time info modal (extracted to SentinelInfoModal.tsx) */} {showSentinelInfo && ( setShowSentinelInfo(false)} /> )} {/* SAR AOI EDITOR — portals to document.body internally */} {(sarAoiEditorOpen || sarAoiDropMode) && ( { setSarAoiEditorOpen(false); setSarAoiDropMode(false); }} onRequestMapPick={() => { setSarAoiEditorOpen(false); setSarAoiDropMode(true); }} pickedCoords={sarAoiDroppedCoords} onPickConsumed={() => setSarAoiDroppedCoords(null)} onAoiListChanged={() => setSarAoiListVersion((v) => v + 1)} dropModeActive={sarAoiDropMode} /> )} {/* MESH TERMINAL */} setTerminalOpen(false)} onDmCount={setDmCount} onSettingsClick={() => setSettingsOpen(true)} /> {showEntityGraph && selectedEntity && isEntityGraphEligible(selectedEntity) && ( setShowEntityGraph(false)} /> )} {/* INFONET TERMINAL */} { setInfonetOpen(false); void endInfonetTerminalSession(); }} onOpenLiveGate={openLiveGateFromShell} onOpenDeadDrop={openDeadDropFromShell} /> {/* BACKEND DISCONNECTED BANNER */} {backendStatus === 'disconnected' && (
{t('backend.offline')}
)} {/* BOTTOM TICKER TOGGLE TAB — moved to center-right to avoid panel overlap */} {/* GLOBAL MARKETS TICKER (BOTTOM ANCHOR) */}
); }