import { useEffect, useState, useCallback, lazy, Suspense } from "react" import { Command as CommandIcon, ChevronLeft, ChevronRight, Loader2, Menu, X } from "lucide-react" import { NAV, type ViewId } from "@/nav" import { CommandPalette } from "@/components/CommandPalette" import { ExpertToggle } from "@/components/ExpertToggle" import { SystemDrawer } from "@/components/SystemDrawer" import { useHealth, useSystemStatus } from "@/lib/queries" import type { Health, SystemStatus } from "@/lib/api" import { useMetricsFeeder } from "@/lib/metricsStore" import { useEventStream } from "@/lib/events" import { cn } from "@/lib/utils" import { CockpitView } from "@/views/cockpit/CockpitView" // Code-Splitting: nur das Cockpit (Startseite) lädt sofort, jede weitere View // kommt als eigenes Chunk erst beim ersten Aufruf — kleineres Start-Bundle. const ModelsView = lazy(() => import("@/views/ModelsView").then((m) => ({ default: m.ModelsView }))) const ConnectView = lazy(() => import("@/views/ConnectView").then((m) => ({ default: m.ConnectView }))) const MemoryView = lazy(() => import("@/views/MemoryView").then((m) => ({ default: m.MemoryView }))) const AgentView = lazy(() => import("@/views/AgentView").then((m) => ({ default: m.AgentView }))) const KonsoleView = lazy(() => import("@/views/KonsoleView").then((m) => ({ default: m.KonsoleView }))) const GuideView = lazy(() => import("@/views/GuideView").then((m) => ({ default: m.GuideView }))) const AuftragsbuchView = lazy(() => import("@/views/AuftragsbuchView").then((m) => ({ default: m.AuftragsbuchView }))) const IdeenView = lazy(() => import("@/views/IdeenView").then((m) => ({ default: m.IdeenView }))) const ChronikView = lazy(() => import("@/views/ChronikView").then((m) => ({ default: m.ChronikView }))) const EigenlebenView = lazy(() => import("@/views/EigenlebenView").then((m) => ({ default: m.EigenlebenView }))) const WissenView = lazy(() => import("@/views/WissenView").then((m) => ({ default: m.WissenView }))) // Dezenter Lade-Zustand während eine View nachgeladen wird (lokales Netz: kaum sichtbar). function ViewLoading() { return (
) } // Tastatur-Hinweis passend zum Betriebssystem (Palette hört auf Ctrl UND Cmd). const IS_MAC = /Mac|iPhone|iPad/.test(navigator.platform) export default function App() { useMetricsFeeder() // sammelt Live-Verlauf global, unabhängig vom aktiven Tab useEventStream() // SSE-Invalidation-Bus (P3a): sofortige Updates, Poller nur Netz // View ist im URL-Hash (#models) verankert → Reload/Teilen/Cmd-Klick funktionieren. const [view, setView] = useState(() => { const h = window.location.hash.slice(1) as ViewId return NAV.some((n) => n.id === h) ? h : "dashboard" }) const navigate = useCallback((v: ViewId) => { if (window.location.hash.slice(1) === v) setView(v) else window.location.hash = v }, []) const [sidebarCollapsed, setSidebarCollapsed] = useState(() => localStorage.getItem("mc_sidebar_collapsed") === "true") const [mobileNavOpen, setMobileNavOpen] = useState(false) const [drawerOpen, setDrawerOpen] = useState(false) const [drawerTab, setDrawerTab] = useState<"maintenance" | "logs">("maintenance") // isError mitnehmen: React Query behält bei Fehlern die letzten Daten — ohne den // Fehler-Blick zeigte die Sidebar bei totem Backend ewig den letzten grünen Stand. const { data: health, isError: backendDown } = useHealth() const { data: sysStatus } = useSystemStatus() useEffect(() => { document.documentElement.classList.add("dark") }, []) useEffect(() => { const handleOpen = (e: Event) => { const customEvent = e as CustomEvent setDrawerTab(customEvent.detail?.tab || "maintenance") setDrawerOpen(true) } window.addEventListener("open-system-drawer", handleOpen) return () => window.removeEventListener("open-system-drawer", handleOpen) }, []) useEffect(() => { const handleNav = (e: Event) => { const v = (e as CustomEvent).detail?.view as ViewId | undefined if (v) navigate(v) } window.addEventListener("mc-navigate", handleNav) return () => window.removeEventListener("mc-navigate", handleNav) }, [navigate]) useEffect(() => { const onHash = () => { const h = window.location.hash.slice(1) as ViewId if (NAV.some((n) => n.id === h)) setView(h) } window.addEventListener("hashchange", onHash) return () => window.removeEventListener("hashchange", onHash) }, []) // Mobile-Navigation schließt nach jedem View-Wechsel (ein Tipp = ein Ziel). useEffect(() => { setMobileNavOpen(false) }, [view]) const active = NAV.find((n) => n.id === view)! return (
{/* Ruhiger Ambient-Hintergrund: statische radial-gradients statt animierter Fullscreen-Blur-Filter — gleicher Look, praktisch keine GPU-Last. */}
setDrawerOpen(false)} defaultTab={drawerTab} /> {/* Sidebar (Desktop, ab md) */} {/* Sidebar (Mobile, Off-Canvas mit Backdrop) */}
setMobileNavOpen(false)} />
{/* Main */}
{active.hint}
Hilfe
}> {view === "dashboard" && navigate(v as ViewId)} />} {view === "ideen" && } {view === "auftraege" && } {view === "models" && } {view === "connect" && } {view === "memory" && } {view === "wissen" && } {view === "chronik" && } {view === "eigenleben" && } {view === "agent" && } {view === "konsole" && } {view === "guide" && }
) } // Sidebar-Inhalt (Logo, Navigation, Status-Fuß) — geteilt zwischen Desktop-Leiste // und Mobile-Off-Canvas, damit beide immer identisch bleiben. function SidebarContent({ collapsed, onToggleCollapse, onClose, view, health, backendDown, sysStatus, }: { collapsed: boolean onToggleCollapse?: () => void onClose?: () => void view: ViewId health: Health | undefined backendDown: boolean sysStatus: SystemStatus | undefined }) { return ( <>
{!collapsed && (
Mission Control
2.0
)}
{onToggleCollapse && ( )} {onClose && ( )}
{collapsed ? (
) : (
{backendDown ? ( Zentrale antwortet nicht ) : health ? ( <> Engine {health.engine_reachable ? "online" : "offline"} {health.brain && !health.brain.ready && ( Hirn offline{health.brain.model ? ` (${health.brain.model})` : ""} )} ) : ( Backend offline )} {sysStatus?.versions && (
MC2: {sysStatus.versions.mc2 ? `${sysStatus.versions.mc2.hash}${sysStatus.versions.mc2.dirty ? "*" : ""}` : "—"}
Engine: {sysStatus.versions.engine?.type === "git" ? `${sysStatus.versions.engine.hash}${sysStatus.versions.engine.dirty ? "*" : ""}` : (sysStatus.versions.engine?.version_text?.split(" ").pop() || "—")}
Hermes Agent: {sysStatus.versions.hermes_agent ? `${sysStatus.versions.hermes_agent.hash}${sysStatus.versions.hermes_agent.dirty ? "*" : ""}` : "—"}
)}
)}
) }