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 (
<>
{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 ? "*" : ""}` : "—"}
)}
)}
>
)
}