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 { api } from '../../api/client'; import { usePresence } from '../../context/PresenceContext'; import { useVisualEffects } from '../../context/VisualEffectsContext'; import ComradeAvatar from '../Presence/ComradeAvatar'; 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'; return 'dashboard'; } const NAV = [ { to: '/dashboard', label: 'Command Deck', icon: 'deck' }, { to: '/crucible', label: 'Crucible', icon: 'crucible' }, { 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' }, ] as const; const DOCS_HREF = '/docs/'; /** Primary tabs on mobile bottom bar — Deck, Crucible, Path Tracer, Forge, Mission Deck */ const MOBILE_PRIMARY = NAV.slice(0, 5); /** Mission Deck, Builds, Emberwake, Calibrate — “More” sheet */ const MOBILE_MORE = NAV.slice(5); function NavIcon({ type }: { type: string }) { switch (type) { case 'deck': return ( ); case 'fleet': return ( ); case 'forge': return ( ); case 'mission': return ( ); case 'builds': return ( ); case 'crucible': return ( ); case 'ember': return ( ); case 'trace': 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 (