// „Geht's Lucy gut?" — bündelt die vielen technischen Status-Signale zu wenigen // Lucy-Fähigkeiten in Klartext, inkl. Gesamt-Verdikt, Speicher-Ampel und einer // konkreten 1-Klick-Reparatur pro Problem. EINE Quelle für das Status-Panel. import { Brain, BookMarked, Eye, Plug, Bot, Mic, ShieldAlert, Server, type LucideIcon } from "lucide-react" import { useHealth, useServices, useModels, useSystemStatus } from "./queries" export type CheckStatus = "ok" | "warn" | "down" | "loading" // Reparatur-Rezept: entweder einen systemd-Dienst neu starten oder ein Modell laden. // WICHTIG: `service` sind die ECHTEN Unit-Namen (nicht die Anzeige-Namen der // Dienste-Karte), damit /api/maintenance/restart sie akzeptiert. export type Repair = | { kind: "restart"; service: string; label: string; needsSudo?: boolean } | { kind: "loadModel"; model: string; label: string } export interface LucyCheck { id: string label: string // Klartext-Name der Fähigkeit icon: LucideIcon status: CheckStatus detail: string // Ein Satz: was ist gerade los critical: boolean // zählt fürs Gesamt-Verdikt „Problem" repair?: Repair } export type Verdict = "loading" | "gut" | "warn" | "problem" export interface MemoryAmpel { status: "ok" | "warn" | "full" | "unknown" usedGb: number totalGb: number pct: number text: string } export interface LucyHealth { verdict: Verdict checks: LucyCheck[] memory: MemoryAmpel problems: LucyCheck[] // kritische Down-Checks (für den Schnellzugriff im Banner) warns: LucyCheck[] // nicht-kritische Hinweise — damit das Banner SAGT, was los ist unreachable: boolean // Zentrale (MC2) antwortet nicht → alle anderen Anzeigen sind veraltet } export function useLucyHealth(): LucyHealth { const { data: health, isError: healthErr } = useHealth() const { data: svc, isError: svcErr } = useServices() const { data: modelsResp } = useModels() const { data: sys } = useSystemStatus() // React Query behält bei Fehlern die letzten Daten — ohne diesen Riegel zeigte die UI // bei totem Backend dauerhaft den letzten grünen Stand („eingefrorene Ampel", Review 15.07.). const unreachable = healthErr || svcErr const loaded = !!health && !!svc // Matching über den ECHTEN systemd-Unit-Namen (Backend liefert `unit`) — vorher wurde // fragil im deutschen Anzeige-Namen gesucht ("gateway (integr" …). const svcOk = (unit: string): boolean | undefined => svc?.services.find((s) => s.unit === unit)?.ok const svcHas = (unit: string): boolean => !!svc?.services.some((s) => s.unit === unit) const engineOk = health?.engine_reachable const brainReady = health?.brain?.ready const brainName = health?.brain?.model || modelsResp?.models.find((m) => m.role === "hermes")?.name || "" const checks: LucyCheck[] = [] // 0) Die Zentrale selbst. Antwortet MC2 nicht, ist ALLES andere Spekulation — // dieser Check übertrumpft die übrigen (die zeigen sonst eingefrorene alte Werte). if (unreachable) { checks.push({ id: "mc2", label: "Steuerpult (MC2)", icon: Server, critical: true, status: "down", detail: "Die Zentrale antwortet nicht — alle Anzeigen hier können veraltet sein. Läuft die Box? Deploy aktiv?", }) } // 1) Lucys Hirn — die wichtigste Fähigkeit. Engine läuft UND das Hirn-Modell ist geladen? checks.push(((): LucyCheck => { const base = { id: "brain", label: "Lucys Hirn", icon: Brain, critical: true } if (!loaded) return { ...base, status: "loading", detail: "Wird geprüft …" } if (engineOk === false) return { ...base, status: "down", detail: "Die Motor-Maschine (Engine) läuft nicht — Lucy kann gerade gar nicht denken.", repair: { kind: "restart", service: "llama-swap", label: "Motor neu starten", needsSudo: true }, } if (brainReady === false) return { ...base, status: "down", detail: "Lucys Hirn ist gerade eingeschlafen — einmal aufwecken.", repair: brainName ? { kind: "loadModel", model: brainName, label: "Hirn aufwecken" } : undefined, } return { ...base, status: "ok", detail: "Wach und ansprechbar." } })()) // 2) Lucys Augen — das Vision-Modell (Rolle `vision`). Seit der Warm-Set-Diät 07.07. ist // ON-DEMAND der NORMALZUSTAND (User-Entscheid: Augen nur, wenn Lucy an ist / IDE sie ruft; // die Lucy-App wärmt sie beim Start selbst an). Ruhende Augen sind darum OK, keine Warnung. checks.push(((): LucyCheck => { const base = { id: "vision", label: "Lucys Augen", icon: Eye, critical: false } if (!loaded || !modelsResp) return { ...base, status: "loading", detail: "Wird geprüft …" } const vm = modelsResp.models.find((m) => m.role === "vision") if (!vm) return { ...base, status: "warn", detail: "Kein Augen-Modell eingerichtet — Lucy kann keine Bilder ansehen." } const warm = (modelsResp.running ?? []).includes(vm.name) return warm ? { ...base, status: "ok", detail: "Sieht gerade zu (geladen, solange Lucy sie nutzt)." } : { ...base, status: "ok", detail: "Augen ruhen — sie laden von selbst, sobald Lucy startet oder ein Bild kommt.", repair: { kind: "loadModel", model: vm.name, label: "Augen wecken" }, } })()) // 3) Lucys Gedächtnis — Mem0-Dienst. checks.push(((): LucyCheck => { const base = { id: "memory", label: "Lucys Gedächtnis", icon: BookMarked, critical: true } const ok = svcOk("mem0-service") if (!loaded || ok === undefined) return { ...base, status: "loading", detail: "Wird geprüft …" } return ok ? { ...base, status: "ok", detail: "Merkt sich Neues und findet Altes wieder." } : { ...base, status: "down", detail: "Lucy kann sich gerade nichts merken — das Gedächtnis antwortet nicht.", repair: { kind: "restart", service: "mem0-service", label: "Gedächtnis neu starten" }, } })()) // 4) Verbindung (LLM-Gateway) — der Denkpfad, über den Apps/IDEs/Lucy die Modelle erreichen. // Seit UMBAU v3 ist das der EIGENE mc2-gateway-Prozess; health.gateway_reachable prüft // ihn jetzt wirklich (statt nur die Engine). Repariert wird die richtige Unit. checks.push(((): LucyCheck => { const base = { id: "gateway", label: "Verbindung (Gateway)", icon: Plug, critical: true } const ok = health ? health.gateway_reachable : svcOk("mc2-gateway") if (!loaded || ok === undefined) return { ...base, status: "loading", detail: "Wird geprüft …" } const unit = svcHas("mc2-gateway") ? "mc2-gateway" : "mission-control-2" return ok ? { ...base, status: "ok", detail: "Apps und IDEs können Lucy erreichen." } : { ...base, status: "down", detail: "Der Modell-Gateway antwortet nicht — Lucy und die IDEs erreichen kein Modell.", repair: { kind: "restart", service: unit, label: "Gateway neu starten" }, } })()) // 4b) Der Wächter (Steward) — überwacht Warm-Set, Health-Flanken und Gedächtnis-Dubletten. // Nicht kritisch fürs Antworten, aber ohne ihn merkt niemand, wenn etwas kippt. checks.push(((): LucyCheck => { const base = { id: "steward", label: "Wächter (Steward)", icon: ShieldAlert, critical: false } if (!loaded) return { ...base, status: "loading", detail: "Wird geprüft …" } if (!svcHas("mc2-steward")) return { ...base, status: "ok", detail: "Auf dieser Installation nicht eingerichtet." } return svcOk("mc2-steward") ? { ...base, status: "ok", detail: "Wacht über Warm-Set, Dienste und Gedächtnis." } : { ...base, status: "warn", detail: "Der Wächter schläft — niemand meldet gerade Ausfälle oder wärmt Modelle nach.", repair: { kind: "restart", service: "mc2-steward", label: "Wächter neu starten" }, } })()) // 5) Lucys Agent (Hermes) — der Dienst, der als Lucy antwortet (Terminal/Telegram/Sprechen). checks.push(((): LucyCheck => { const base = { id: "agent", label: "Lucys Agent (Hermes)", icon: Bot, critical: true } const ok = svcOk("hermes-gateway") // unit-Name (identisch zum alten Namens-Match, jetzt exakt) if (!loaded || ok === undefined) return { ...base, status: "loading", detail: "Wird geprüft …" } return ok ? { ...base, status: "ok", detail: "Lucy ist bereit zu reden und zu handeln." } : { ...base, status: "down", detail: "Lucys Agent ist offline — sie reagiert gerade nicht.", repair: { kind: "restart", service: "hermes-gateway", label: "Agent neu starten" }, } })()) // 6) Lucys Stimme — Voice-Dienst (STT/TTS). Nicht kritisch: Chat geht auch ohne. checks.push(((): LucyCheck => { const base = { id: "voice", label: "Lucys Stimme", icon: Mic, critical: false } const ok = svcOk("voice-service") if (!loaded || ok === undefined) return { ...base, status: "loading", detail: "Wird geprüft …" } return ok ? { ...base, status: "ok", detail: "Lucy kann hören und sprechen." } : { ...base, status: "warn", detail: "Sprechen ist gerade aus — Tippen geht weiter normal.", repair: { kind: "restart", service: "voice-service", label: "Stimme neu starten" }, } })()) // Speicher-Ampel = System-RAM, DIESELBE Quelle wie die RAM-Zeile in der // System-Status-Kachel. Vorher zeigte die Pille den GTT-Pool (GPU-Sicht auf den // Unified-Memory) — der weicht auf der APU vom System-RAM ab, wodurch „Box gesund" // und „System-Status" zwei verschiedene Speicher-Zahlen zeigten. Eine Box, eine Zahl. // Schwellen am RAM-Charakter geeicht: 4 Modelle warm ≈ 78 % gilt als gesund → grün. const total = sys?.ram?.total ?? 0 const used = sys?.ram?.used ?? 0 const pct = sys?.ram?.percent ?? (total > 0 ? Math.min(100, (used / total) * 100) : 0) const B = 1024 ** 3 const memory: MemoryAmpel = !total ? { status: "unknown", usedGb: 0, totalGb: 0, pct: 0, text: "Speicher-Auslastung unbekannt." } : pct >= 93 ? { status: "full", usedGb: used / B, totalGb: total / B, pct, text: "Speicher fast voll — Lucy könnte langsamer werden." } : pct >= 85 ? { status: "warn", usedGb: used / B, totalGb: total / B, pct, text: "Speicher gut gefüllt — noch okay." } : { status: "ok", usedGb: used / B, totalGb: total / B, pct, text: "Genug Speicher frei." } const anyLoading = checks.some((c) => c.status === "loading") const problems = checks.filter((c) => c.critical && c.status === "down") const warns = checks.filter((c) => c.status === "warn" || (!c.critical && c.status === "down")) const memBad = memory.status === "full" const verdict: Verdict = anyLoading && !problems.length ? "loading" : problems.length ? "problem" : warns.length || memBad ? "warn" : "gut" return { verdict, checks, memory, problems, warns, unreachable } }