import { ReactNode, memo, useEffect, useMemo, useRef, useState } from 'react'; import { NavLink, useLocation } from 'react-router-dom'; import AmbientBackground from '../Ambient/AmbientBackground'; import SystemStatusBar from '../Visual/SystemStatusBar'; import { useWebSocket } from '../../hooks/useWebSocket'; import { useIsMobileLayout } from '../../hooks/useMediaQuery'; import MatrixRain from './MatrixRain'; import afLogo from '../../assets/af-logo.png'; import CursorFire from '../Visual/CursorFire'; import SacredGeometryLayer from '../Visual/sacredGeometry/SacredGeometryLayer'; import { SacredMotif } from '../Visual/sacredGeometry/motifs'; import SetupBanner from '../SetupBanner'; import { getSetupStatus } from '../../help/setupStatus'; import { resolvePageWeather } from '../../help/pageWeather'; import { mergeScoutBiomeWeather, type ScoutConstellationSnapshot } from '../../help/scoutBiomeWeather'; import { isDashboardRoute } from '../../help/routeEffects'; import { api } from '../../api/client'; import { usePresence } from '../../context/PresenceContext'; import { useVisualEffects } from '../../context/VisualEffectsContext'; import ComradeAvatar from '../Presence/ComradeAvatar'; import { formatAppVersion } from '../../help/appVersion'; import type { ServerConfig, ServerInfo } from '../../types'; import '../Presence/Presence.css'; import './Layout.css'; import './MobileNav.css'; import '../../styles/operatorDeck.css'; interface LayoutProps { children: ReactNode; } function operatorDeckId(pathname: string): string { const path = pathname.split('?')[0].replace(/\/$/, '') || '/'; if (path.startsWith('/mission-deck')) return 'mission-deck'; if (path.startsWith('/forge') || path.startsWith('/builder')) return 'forge'; if (path.startsWith('/crucible')) return 'crucible'; if (path.startsWith('/emberwake') || path.startsWith('/spread')) return 'emberwake'; if (path.startsWith('/builds')) return 'builds'; if (path.startsWith('/settings')) return 'settings'; if (path.startsWith('/pathtracer')) return 'pathtracer'; if (path.startsWith('/lotl-timeline') || path.startsWith('/onion')) return 'lotl-timeline'; if (path.startsWith('/roi')) return 'roi'; if (path.startsWith('/activity')) return 'activity'; if (path.startsWith('/seer')) return 'seer'; return 'dashboard'; } type NavItem = { readonly to: string; readonly label: string; readonly icon: string; readonly glow?: boolean }; const NAV_BASE: readonly NavItem[] = [ { to: '/dashboard', label: 'Command Deck', icon: 'deck' }, { to: '/crucible', label: 'Crucible', icon: 'crucible' }, { to: '/activity', label: 'Activity Feed', icon: 'activity' }, { to: '/roi', label: 'ROI Intelligence', icon: 'roi' }, { to: '/lotl-timeline', label: 'Onion', icon: 'onion' }, { to: '/pathtracer', label: 'Path Tracer', icon: 'trace' }, { to: '/forge', label: 'Forge', icon: 'forge' }, { to: '/mission-deck', label: 'Mission Deck', icon: 'mission', glow: true }, { to: '/builds', label: 'Builds', icon: 'builds' }, { to: '/emberwake', label: 'Emberwake', icon: 'ember' }, { to: '/settings', label: 'Calibrate', icon: 'gear' }, ]; const SEER_NAV: NavItem = { to: '/seer', label: 'Seer', icon: 'seer' }; function buildNav(aiControlEnabled: boolean): NavItem[] { if (!aiControlEnabled) { return [...NAV_BASE]; } const items: NavItem[] = [...NAV_BASE]; const calibrateIdx = items.findIndex((i) => i.to === '/settings'); items.splice(calibrateIdx, 0, SEER_NAV); return items; } const NAV = NAV_BASE; const DOCS_HREF = '/docs/'; /** Primary tabs on mobile bottom bar — Deck, Crucible, Activity, ROI, Onion */ const MOBILE_PRIMARY = NAV.slice(0, 5); /** Path Tracer, Forge, Mission Deck, Builds, Emberwake, Calibrate — "More" sheet */ const MOBILE_MORE = NAV.slice(5); function NavIcon({ type }: { type: string }) { switch (type) { case 'deck': return ( ); case 'roi': return ( ); case 'activity': return ( ); case 'fleet': return ( ); case 'forge': return ( ); case 'mission': return ( ); case 'builds': return ( ); case 'crucible': return ( ); case 'onion': return ( ); case 'ember': return ( ); case 'trace': return ( ); case 'seer': return ( ); case 'docs': return ( ); default: return ( ); } } function formatHashrate(hs: number): string { if (hs >= 1_000_000) return `${(hs / 1_000_000).toFixed(2)} MH/s`; if (hs >= 1_000) return `${(hs / 1_000).toFixed(1)} KH/s`; return `${hs.toFixed(0)} H/s`; } const FleetReadout = memo(function FleetReadout() { const { agents } = useWebSocket(); const { online, total, totalHashrate, fillPct } = useMemo(() => { const on = agents.filter((a) => a.status === 'online').length; const tot = agents.length; const hr = agents.reduce((sum, a) => sum + (a.hashrate_15s ?? a.hashrate_15m ?? 0), 0); return { online: on, total: tot, totalHashrate: hr, fillPct: tot > 0 ? Math.round((on / tot) * 100) : 0, }; }, [agents]); // Tick animation: flash hashrate value whenever it meaningfully changes const [flash, setFlash] = useState(false); const prevHash = useRef(0); useEffect(() => { if (Math.abs(totalHashrate - prevHash.current) > 1) { prevHash.current = totalHashrate; setFlash(true); const t = setTimeout(() => setFlash(false), 600); return () => clearTimeout(t); } }, [totalHashrate]); return (