import { useEffect, useState, useCallback, Suspense, lazy } from "react"
import { Link, Outlet, useNavigate, useRouterState } from "@tanstack/react-router"
import { Command as CommandIcon, ChevronLeft, ChevronRight, Loader2, Menu, X } from "lucide-react"
import { NAV, navFuerPfad } from "@/nav"
import { ExpertToggle } from "@/components/ExpertToggle"
import { useHealth, useSystemStatus } from "@/lib/queries"
import type { Health } from "@/lib/api"
import { useMetricsFeeder } from "@/lib/metricsStore"
import { useEventStream } from "@/lib/events"
import { useMC, usePaletteOffen, useSchieneEingeklappt } from "@/app/store"
import { Statusleiste } from "@/app/shell/Statusleiste"
import { Meldungen } from "@/app/shell/Meldungen"
import { cn } from "@/lib/utils"
// Beide sind beim Start UNSICHTBAR und gehoeren deshalb nicht ins Start-Buendel (P2):
// die Schublade zieht Wartung, Logs, Einstellungen und den Update-Dialog mit, die Palette
// bringt cmdk mit. Zusammen waren das rund 30 kB gzip, die niemand beim ersten Bild braucht.
// Der Router kostete P2 ungefaehr dasselbe — so bleibt das Buendel-Budget der Ampel scharf,
// statt es einfach hochzusetzen.
const SystemDrawer = lazy(() => import("@/components/SystemDrawer").then((m) => ({ default: m.SystemDrawer })))
const CommandPalette = lazy(() => import("@/components/CommandPalette").then((m) => ({ default: m.CommandPalette })))
// 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)
/**
* Die Shell: Schiene, Kopfzeile, System-Schublade — alles, was über JEDER Ansicht liegt.
*
* v3-Umbau P2 (28.08.2026): Hier stand vorher der Ansichts-Umschalter
* (`{view === "ideen" && }`), gespeist aus einem `useState`, das am URL-Hash
* hing. Das funktionierte für Reload und Lesezeichen — aber nur auf View-Ebene. Jetzt
* rendert der Router in den ``, und der Zustand DARUNTER (offene Idee, Reiter,
* Schublade) liegt in echten Suchparametern. Siehe app/router.tsx.
*/
export function Shell() {
useMetricsFeeder() // sammelt Live-Verlauf global, unabhängig vom aktiven Tab
useEventStream() // SSE-Invalidation-Bus (P3a): sofortige Updates, Poller nur Netz
const navigate = useNavigate()
const pfad = useRouterState({ select: (s) => s.location.pathname })
// Die Schublade liegt über allem und gehört deshalb in die Wurzel-Suche (?system=).
// Vorher war sie lokaler State und wurde per CustomEvent von außen aufgestoßen —
// nicht teilbar, nicht reload-fest.
const drawerTab = useRouterState({
select: (s) => (s.location.search as { system?: "wartung" | "logs" }).system,
})
// v3-Umbau P3: Schienen-Zustand und Palette liegen jetzt im Store (Befund B-08/B-09).
// Vorher: ein localStorage-Zugriff mitten in useState plus ein GEFAELSCHTES
// KeyboardEvent, das der Kopfzeilen-Knopf aufs Dokument feuerte.
const schieneEingeklappt = useSchieneEingeklappt()
const schieneUmschalten = useMC((st) => st.schieneUmschalten)
const paletteOffen = usePaletteOffen()
const palette = useMC((st) => st.palette)
const [mobileNavOpen, setMobileNavOpen] = useState(false)
// Die Palette wird beim ERSTEN Ruf geladen und bleibt dann gemountet — sie haelt ihren
// Inhalt und soll nicht bei jedem Strg+K neu nachgeladen werden.
const [paletteBereit, setPaletteBereit] = useState(false)
useEffect(() => {
if (paletteOffen) setPaletteBereit(true)
}, [paletteOffen])
useEffect(() => {
const taste = (e: KeyboardEvent) => {
if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "k") {
e.preventDefault()
palette(!useMC.getState().ui.paletteOffen)
}
}
document.addEventListener("keydown", taste)
return () => document.removeEventListener("keydown", taste)
}, [palette])
const schubladeSetzen = useCallback((tab: "wartung" | "logs" | undefined) => {
navigate({ to: ".", search: (alt) => ({ ...alt, system: tab }), replace: true })
}, [navigate])
// 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()
useEffect(() => {
document.documentElement.classList.add("dark")
}, [])
// Hier hingen bis P2 zwei CustomEvent-Zuhörer (`open-system-drawer`, `mc-navigate`),
// über die andere Ansichten die Shell fernsteuerten. In P3 sind sie ersatzlos weg:
// Wer die Schublade öffnen will, setzt `?system=`; wer springen will, navigiert. Der
// Zustand liegt damit da, wo man ihn sehen und teilen kann (Befund B-08).
// Mobile-Navigation schließt nach jedem View-Wechsel (ein Tipp = ein Ziel).
useEffect(() => { setMobileNavOpen(false) }, [pfad])
const active = navFuerPfad(pfad)
return (
{/* Ruhiger Ambient-Hintergrund: statische radial-gradients statt animierter
Fullscreen-Blur-Filter — gleicher Look, praktisch keine GPU-Last. */}
{/* Beilaeufige Rueckmeldungen — EINE Stelle, und endlich mit aria-live (B-11). */}
{/* Erst laden, wenn wirklich gebraucht — ohne Platzhalter, beide sind Overlays. */}
{paletteBereit && }
{drawerTab && (
schubladeSetzen(undefined)}
defaultTab={drawerTab === "logs" ? "logs" : "maintenance"}
/>
)}
{/* Sidebar (Desktop, ab md) */}
{/* Sidebar (Mobile, Off-Canvas mit Backdrop) */}
{/* Bahnbreiten-Deckel (Befund B-10): Ohne ihn zieht sich das Cockpit auf einem
3440-px-Ultrawide auf ueber 3 000 px Breite; die Wurzel-Schriftgroesse waechst
dabei mit und macht die Zeilen GROESSER statt lesbarer. 1600 px sind die Grenze,
ab der eine Zeile nicht mehr am Stueck ueberblickt wird — der Rest bleibt Rand. */}
}>
{/* Zeigt den Zustand der Box IMMER, ohne den Arbeitsbereich zu blockieren (§4.1). */}
)
}
// Sidebar-Inhalt (Logo, Navigation, Status-Fuß) — geteilt zwischen Desktop-Leiste
// und Mobile-Off-Canvas, damit beide immer identisch bleiben.
function SidebarContent({
collapsed, onToggleCollapse, onClose, pfad, health, backendDown,
}: {
collapsed: boolean
onToggleCollapse?: () => void
onClose?: () => void
pfad: string
health: Health | undefined
backendDown: boolean
}) {
const aktiv = navFuerPfad(pfad)
// Kein Prop-Drilling mehr: derselbe Query-Key, also dieselbe Anfrage — TanStack Query
// dedupliziert. Die Versionen stehen bewusst hier unten und nicht in der Statusleiste:
// Man schaut sie selten an, und die Leiste soll knapp bleiben.
const { data: sysStatus } = useSystemStatus()
return (
<>