import { useState } from "react" import { Boxes, Brain, Server, ShieldAlert, Plug, HeartPulse, AlertTriangle, Loader2, Wrench, Check, HardDrive, ChevronRight, Inbox, } from "lucide-react" import { api } from "@/lib/api" import { useModels, useServices, useUpdates, useMemory, useAgentStatus, useConnectHealth, useAuftragsbuch, useQueryClient, qk, } from "@/lib/queries" import { useLucyHealth, type Repair } from "@/lib/useLucyHealth" import { useDialog } from "@/lib/useDialog" import { cn } from "@/lib/utils" import { CockpitTile } from "./CockpitTile" import { MorgenlageCard } from "./MorgenlageCard" import { ErinnerungenCard } from "./ErinnerungenCard" import { SystemStatusCard } from "@/components/dashboard/SystemStatusCard" import { TokenPerformanceCard } from "@/components/dashboard/TokenPerformanceCard" import { LatencyCard } from "@/components/dashboard/LatencyCard" // Öffnet die bestehende System-Schublade (Pflege/Logs) via globalem Event. const openDrawer = (tab: "maintenance" | "logs") => window.dispatchEvent(new CustomEvent("open-system-drawer", { detail: { tab } })) // Cockpit = die Startseite. Ruhige Übersicht: ehrliche Status-Zeile oben, „Braucht // dich"-Bereich für offene Aktionen, darunter die Bereichs-Kacheln (je EINE Live-Zahl). // Überblick außen, Tiefe hinter der Kachel — Daten/Aktionen aus den IST-Hooks. export function CockpitView({ onNavigate }: { onNavigate: (v: string) => void }) { const { data: models } = useModels(4_000) const { data: svc } = useServices(4_000) const { data: updates } = useUpdates(8_000) const { data: memory } = useMemory() const { data: agent } = useAgentStatus() const { data: connect } = useConnectHealth() const { data: auftraege } = useAuftragsbuch(20_000) const health = useLucyHealth() const { showAlert, dialogElement } = useDialog() const qc = useQueryClient() const [busy, setBusy] = useState>({}) async function runRepair(id: string, repair: Repair) { setBusy((b) => ({ ...b, [id]: true })) try { if (repair.kind === "loadModel") { await api(`/api/models/${encodeURIComponent(repair.model)}/load`, { method: "POST" }) } else { const r = await api<{ ok: boolean; err?: string }>("/api/maintenance/restart", { method: "POST", body: JSON.stringify({ service: repair.service }), }) if (!r.ok) showAlert("Reparatur nicht ganz geklappt", (r.err || "Unbekannter Fehler") + (repair.needsSudo ? "\n\nTipp: Dafür wird evtl. das Box-Passwort gebraucht (oben rechts hinterlegen)." : "")) } qc.invalidateQueries({ queryKey: qk.health }) qc.invalidateQueries({ queryKey: qk.services }) qc.invalidateQueries({ queryKey: qk.models }) } catch (e: any) { showAlert("Reparatur fehlgeschlagen", String(e?.message || e)) } finally { setBusy((b) => ({ ...b, [id]: false })) } } // ── Kachel-Zahlen aus den IST-Daten ableiten ────────────────────────────── const running = models?.running?.length ?? 0 const svcOk = svc?.services.filter((s) => s.ok).length ?? 0 const svcTotal = svc?.services.length ?? 0 const svcErrors = svcTotal - svcOk const notes = memory?.length ?? 0 const pendingCount = (updates ? (updates.os > 0 ? 1 : 0) + (updates.engine > 0 ? 1 : 0) + (updates.swap > 0 ? 1 : 0) + (updates.models > 0 ? 1 : 0) : 0) + (updates?.components?.filter((c) => c.update === true).length ?? 0) const connectOk = connect?.gateway.ok && connect?.memory.ok const openAuftraege = auftraege?.available ? auftraege.open_count : 0 // Zusätzliche reaktive Livedaten für die Kacheln const activeBrain = models?.models?.find((m: any) => m.role === "hermes") const brainName = activeBrain?.name ? activeBrain.name.split("/").pop()?.replace(/\.gguf$/i, "") : "" const autoNotes = memory ? memory.filter((m: any) => ["auto", "agent", "hermes"].includes(m.source)).length : 0 const failedSvc = svc?.services.filter((s: any) => !s.ok).map((s: any) => s.name).join(", ") const upParts = updates ? [ updates.os > 0 && "OS", updates.engine > 0 && "Engine", updates.swap > 0 && "Swap", updates.models > 0 && "Modelle" ].filter(Boolean) : [] const updateSummary = upParts.length > 0 ? upParts.join(" + ") : "" return (
{/* Kopf */}

Cockpit

Deine Box auf einen Blick — Details hinter jeder Kachel.

{/* Main Grid: 2/3 Links (Inhalte & Performance), 1/3 Rechts (Shortcuts & Kalender) */}
{/* Hauptspalte Links */}
{/* Status-Zeile */} {/* Morgenlage — das jüngste Nacht-Gutachten (verschwindet, wenn keines da ist) */} {/* Leistung — die Live-Graphen */}
Leistung
{/* Bereichs-Kacheln (6 statt 7 - Logs ist in die Werkzeug-Liste gewandert) */}
Bereiche
0 ? "warn" : "ok"} hint={openAuftraege > 0 ? `${openAuftraege} offene Patches` : "Keine ausstehenden Vorschläge"} onClick={() => onNavigate("auftraege")} /> 0 ? "ok" : "muted"} hint={brainName ? `Hirn: ${brainName}` : "Kein Hermes-Modell geladen"} onClick={() => onNavigate("models")} /> onNavigate("memory")} /> 0 ? "warn" : "ok"} hint={failedSvc ? `Ausfall: ${failedSvc}` : "Alle Dienste online"} onClick={() => openDrawer("maintenance")} /> 0 ? pendingCount : "0") : "…"} unit={pendingCount > 0 ? "bereit" : "aktuell"} tone={!updates ? "loading" : pendingCount > 0 ? "warn" : "ok"} hint={pendingCount > 0 ? `Verfügbar: ${updateSummary}` : "Alles auf dem neuesten Stand"} onClick={() => openDrawer("maintenance")} /> onNavigate("connect")} />
{/* Seitenspalte Rechts */}
{/* Handlungsbedarf (NeedsYou) */} {/* Anstehendes — Erinnerungen & Routinen */}
Anstehendes
{/* Werkzeuge & Shortcuts */}
Werkzeuge & Diagnose
{dialogElement}
) } function ZoneLabel({ children }: { children: React.ReactNode }) { return

{children}

} // Ehrliche Status-Zeile: großes Verdikt + Speicher-Pille. Erzählrahmen = Box. function StatusLine({ verdict, memory, warns }: { verdict: ReturnType["verdict"] memory: ReturnType["memory"] warns: ReturnType["warns"] }) { const warnText = warns.length ? `${warns[0].label}: ${warns[0].detail}${warns.length > 1 ? ` (+${warns.length - 1} weitere)` : ""}` : memory.status === "full" ? memory.text : "Nichts Schlimmes — nur ein Hinweis." const b = { loading: { ring: "border-border/60 bg-card/45", icon: Loader2, iconCls: "text-muted-foreground animate-spin", title: "Box wird geprüft …", sub: "Einen Moment." }, gut: { ring: "border-emerald-500/40 bg-emerald-500/5", icon: HeartPulse, iconCls: "text-emerald-400", title: "Box gesund", sub: "Alle wichtigen Dienste laufen." }, warn: { ring: "border-amber-500/40 bg-amber-500/5", icon: AlertTriangle, iconCls: "text-amber-400", title: "Kleinigkeit an der Box", sub: warnText }, problem: { ring: "border-red-500/50 bg-red-500/5", icon: AlertTriangle, iconCls: "text-red-400", title: "Box braucht Hilfe", sub: "Ein wichtiger Dienst hakt." }, }[verdict] const Icon = b.icon const memTone = { ok: "text-emerald-400", warn: "text-amber-400", full: "text-red-400", unknown: "text-muted-foreground" }[memory.status] const memBar = { ok: "bg-emerald-500", warn: "bg-amber-500", full: "bg-red-500", unknown: "bg-muted-foreground/40" }[memory.status] return (

{b.title}

{b.sub}

{/* Speicher-Pille */}
Speicher {memory.status === "unknown" ? "—" : `${memory.usedGb.toFixed(0)} / ${memory.totalGb.toFixed(0)} GB`}
) } // „Braucht dich": kritische Probleme (mit 1-Klick-Reparatur) + bereitliegende Updates // + offene Vorschläge im Auftragsbuch. function NeedsYou({ problems, pendingCount, openAuftraege, busy, onRepair, onNavigate, }: { problems: ReturnType["problems"] pendingCount: number openAuftraege: number busy: Record onRepair: (id: string, r: Repair) => void onNavigate: (v: string) => void }) { const hasUpdates = pendingCount > 0 const hasAuftraege = openAuftraege > 0 const nothing = problems.length === 0 && !hasUpdates && !hasAuftraege return (
{nothing ? (

Nichts zu tun

Alles läuft. Du musst gerade nichts abnicken.

) : (
{problems.map((c) => (

{c.label}

{c.detail}

{c.repair && ( )}
))} {hasAuftraege && ( )} {hasUpdates && ( )}
)}
) } function ToolListCard({ agent, svcErrors, onNavigate }: { agent: any svcErrors: number onNavigate: (v: string) => void }) { const tools = [ { label: "Hermes Agent", status: agent ? (agent.gateway_reachable ? "Bereit" : "Offline") : "…", tone: !agent ? "loading" : agent.gateway_reachable ? "ok" : "alert", action: () => onNavigate("agent"), }, { label: "Direkte Box-Shell (SSH)", status: agent ? (agent.box_console_reachable ? "Bereit" : "Offline") : "…", tone: !agent ? "loading" : agent.box_console_reachable ? "ok" : "warn", action: () => onNavigate("konsole"), }, { label: "Systemlogs & Diagnose", status: svcErrors > 0 ? `${svcErrors} Fehler` : "Keine Fehler", tone: svcErrors > 0 ? "alert" : "ok", action: () => openDrawer("logs"), }, { label: "Interaktives Terminal", status: "Öffnen", tone: "muted", action: () => onNavigate("terminal"), }, { label: "Wissens-Vault", status: "Öffnen", tone: "muted", action: () => onNavigate("wissen"), }, { label: "Chronik & Zeitmaschine", status: "Öffnen", tone: "muted", action: () => onNavigate("chronik"), }, ] return (
{tools.map((t, idx) => { const dotColor = { loading: "bg-muted-foreground/45 animate-pulse", ok: "bg-emerald-500 shadow-[0_0_8px_rgba(16,185,129,0.4)]", warn: "bg-amber-500 shadow-[0_0_8px_rgba(245,158,11,0.4)]", alert: "bg-red-500 shadow-[0_0_8px_rgba(239,68,68,0.4)]", muted: "bg-muted-foreground/30", }[t.tone] return ( ) })}
) }