Wartung: Cockpit-Restrukturierung (zweispaltiges Grid, ToolListCard, ungenutzte Imports entfernt)

This commit is contained in:
Hitonabi
2026-07-09 12:03:11 +02:00
parent 0dc9caa955
commit 51c0c868b2
6 changed files with 317 additions and 263 deletions
+186 -127
View File
@@ -1,15 +1,14 @@
import { useState } from "react"
import {
Boxes, Brain, Server, ShieldAlert, Plug, Bot, TerminalSquare, ScrollText,
Boxes, Brain, Server, ShieldAlert, Plug,
HeartPulse, AlertTriangle, Loader2, Wrench, Check, HardDrive, ChevronRight,
Inbox, History, Library,
Inbox,
} from "lucide-react"
import { api } from "@/lib/api"
import {
useModels, useServices, useUpdates, useMemory, useAgentStatus, useConnectHealth,
useAuftragsbuch, useQueryClient, qk,
} from "@/lib/queries"
import { SquareTerminal } from "lucide-react"
import { useLucyHealth, type Repair } from "@/lib/useLucyHealth"
import { useDialog } from "@/lib/useDialog"
import { cn } from "@/lib/utils"
@@ -90,7 +89,7 @@ export function CockpitView({ onNavigate }: { onNavigate: (v: string) => void })
const updateSummary = upParts.length > 0 ? upParts.join(" + ") : ""
return (
<div className="space-y-7">
<div className="space-y-6">
{/* Kopf */}
<div>
<h1 className="bg-gradient-to-r from-foreground via-foreground to-primary bg-clip-text font-space text-2xl font-bold tracking-tight text-transparent">
@@ -99,124 +98,103 @@ export function CockpitView({ onNavigate }: { onNavigate: (v: string) => void })
<p className="text-sm text-muted-foreground">Deine Box auf einen Blick Details hinter jeder Kachel.</p>
</div>
{/* Ehrliche Status-Zeile */}
<StatusLine verdict={health.verdict} memory={health.memory} warns={health.warns} />
{/* Morgenlage — das jüngste Nacht-Gutachten (verschwindet, wenn keines da ist) */}
<MorgenlageCard />
{/* „Braucht dich" — offene Aktionen, sonst Ruhe */}
<NeedsYou
problems={health.problems}
pendingCount={pendingCount}
openAuftraege={openAuftraege}
busy={busy}
onRepair={runRepair}
onNavigate={onNavigate}
/>
{/* Bereichs-Kacheln */}
<section>
<ZoneLabel>Bereiche</ZoneLabel>
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
<CockpitTile
icon={Inbox} title="Auftragsbuch" value={auftraege?.available ? openAuftraege : "—"}
unit={openAuftraege === 1 ? "Vorschlag" : "Vorschläge"}
tone={!auftraege ? "loading" : openAuftraege > 0 ? "warn" : "ok"}
hint={openAuftraege > 0 ? `${openAuftraege} offene Patches prüfen` : "Keine ausstehenden Vorschläge"}
onClick={() => onNavigate("auftraege")}
/>
<CockpitTile
icon={Boxes} title="Modelle" value={running} unit="warm"
tone={running > 0 ? "ok" : "muted"}
hint={brainName ? `Hirn: ${brainName}` : "Kein Hermes-Modell geladen"}
onClick={() => onNavigate("models")}
/>
<CockpitTile
icon={Brain} title="Gedächtnis" value={memory ? notes : "…"} unit="Notizen"
tone={memory ? "ok" : "loading"}
hint={memory ? `${notes} Fakten (${autoNotes} auto gelernt)` : "Gedächtnis-Pool laden..."}
onClick={() => onNavigate("memory")}
/>
<CockpitTile
icon={Server} title="Dienste" value={svc ? `${svcOk}/${svcTotal}` : "…"} unit="laufen"
tone={!svc ? "loading" : svcErrors > 0 ? "warn" : "ok"}
hint={failedSvc ? `Ausfall: ${failedSvc}` : "Alle Dienste online"}
onClick={() => openDrawer("maintenance")}
/>
<CockpitTile
icon={ShieldAlert} title="Updates" value={updates ? (pendingCount > 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")}
/>
<CockpitTile
icon={Plug} title="Verbinden" value="Zed" unit={connectOk ? "· bereit" : ""}
tone={!connect ? "loading" : connectOk ? "ok" : "warn"}
hint={agent?.pc_executor_reachable ? "IDE & PC online (:7777)" : "IDE-Kopplung (PC offline)"}
onClick={() => onNavigate("connect")}
/>
<CockpitTile
icon={ScrollText} title="Logs" value={svc ? svcErrors : "…"} unit={svcErrors === 1 ? "Fehler" : "Fehler"}
tone={!svc ? "loading" : svcErrors > 0 ? "alert" : "ok"}
hint={svcErrors > 0 ? `${svcErrors} Dienst-Fehler gefunden` : "Keine Log-Warnungen"}
onClick={() => openDrawer("logs")}
{/* Top Grid: StatusLine (2/3) und NeedsYou (1/3) */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 items-stretch">
<div className="lg:col-span-2">
<StatusLine verdict={health.verdict} memory={health.memory} warns={health.warns} />
</div>
<div>
<NeedsYou
problems={health.problems}
pendingCount={pendingCount}
openAuftraege={openAuftraege}
busy={busy}
onRepair={runRepair}
onNavigate={onNavigate}
/>
</div>
</section>
</div>
{/* Anstehendes — Erinnerungen & Routinen (GUI-Hälfte der Wecker-API) */}
<section>
<ZoneLabel>Anstehendes</ZoneLabel>
<ErinnerungenCard />
</section>
{/* Main Grid: 2/3 Links (Inhalte & Performance), 1/3 Rechts (Shortcuts & Kalender) */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 items-start">
{/* Hauptspalte Links */}
<div className="lg:col-span-2 space-y-6">
{/* Morgenlage — das jüngste Nacht-Gutachten (verschwindet, wenn keines da ist) */}
<MorgenlageCard />
{/* Leistung — die Live-Graphen zurück auf der Startseite (User-Wunsch 07.07.:
System-Verlauf, tok/s und Turn-Latenz waren nach dem UI-v3-Umbau nur noch in
der alten Zentrale erreichbar). Karten sind autark (eigene Hooks/Polling). */}
<section>
<ZoneLabel>Leistung</ZoneLabel>
<div className="grid gap-4 lg:grid-cols-2">
<SystemStatusCard />
<TokenPerformanceCard />
</div>
<div className="mt-4">
<LatencyCard />
</div>
</section>
{/* Bereichs-Kacheln (6 statt 7 - Logs ist in die Werkzeug-Liste gewandert) */}
<section>
<ZoneLabel>Bereiche</ZoneLabel>
<div className="grid gap-4 grid-cols-2 md:grid-cols-3">
<CockpitTile
icon={Inbox} title="Auftragsbuch" value={auftraege?.available ? openAuftraege : "—"}
unit={openAuftraege === 1 ? "Vorschlag" : "Vorschläge"}
tone={!auftraege ? "loading" : openAuftraege > 0 ? "warn" : "ok"}
hint={openAuftraege > 0 ? `${openAuftraege} offene Patches` : "Keine ausstehenden Vorschläge"}
onClick={() => onNavigate("auftraege")}
/>
<CockpitTile
icon={Boxes} title="Modelle" value={running} unit="warm"
tone={running > 0 ? "ok" : "muted"}
hint={brainName ? `Hirn: ${brainName}` : "Kein Hermes-Modell geladen"}
onClick={() => onNavigate("models")}
/>
<CockpitTile
icon={Brain} title="Gedächtnis" value={memory ? notes : "…"} unit="Notizen"
tone={memory ? "ok" : "loading"}
hint={memory ? `${notes} Fakten (${autoNotes} auto gelernt)` : "Gedächtnis-Pool laden..."}
onClick={() => onNavigate("memory")}
/>
<CockpitTile
icon={Server} title="Dienste" value={svc ? `${svcOk}/${svcTotal}` : "…"} unit="laufen"
tone={!svc ? "loading" : svcErrors > 0 ? "warn" : "ok"}
hint={failedSvc ? `Ausfall: ${failedSvc}` : "Alle Dienste online"}
onClick={() => openDrawer("maintenance")}
/>
<CockpitTile
icon={ShieldAlert} title="Updates" value={updates ? (pendingCount > 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")}
/>
<CockpitTile
icon={Plug} title="Verbinden" value="Zed" unit={connectOk ? "· bereit" : ""}
tone={!connect ? "loading" : connectOk ? "ok" : "warn"}
hint={agent?.pc_executor_reachable ? "IDE & PC online (:7777)" : "IDE-Kopplung (PC offline)"}
onClick={() => onNavigate("connect")}
/>
</div>
</section>
{/* Sekundäre Bereiche */}
<section>
<ZoneLabel>Mehr</ZoneLabel>
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
<CockpitTile
icon={Bot} title="Hermes" value={agent ? (agent.gateway_reachable ? "bereit" : "offline") : "…"}
tone={!agent ? "loading" : agent.gateway_reachable ? "ok" : "alert"} hint="Agent-Status & Verdrahtung"
onClick={() => onNavigate("agent")}
/>
<CockpitTile
icon={TerminalSquare} title="Terminal" value="öffnen"
tone="muted" hint="Interaktives Agent-Terminal"
onClick={() => onNavigate("terminal")}
/>
<CockpitTile
icon={SquareTerminal} title="Konsole" value={agent ? (agent.box_console_reachable ? "bereit" : "offline") : "…"}
tone={!agent ? "loading" : agent.box_console_reachable ? "ok" : "warn"} hint="Direkte Box-Shell (SSH)"
onClick={() => onNavigate("konsole")}
/>
<CockpitTile
icon={Library} title="Wissen" value="öffnen"
tone="muted" hint="Lucys Wissens-Vault (Traum-Notizen)"
onClick={() => onNavigate("wissen")}
/>
<CockpitTile
icon={History} title="Chronik" value="öffnen"
tone="muted" hint="Autonome Taten + Zeitmaschine"
onClick={() => onNavigate("chronik")}
/>
{/* Leistung — die Live-Graphen */}
<section>
<ZoneLabel>Leistung</ZoneLabel>
<div className="grid gap-4 md:grid-cols-2">
<SystemStatusCard />
<TokenPerformanceCard />
</div>
<div className="mt-4">
<LatencyCard />
</div>
</section>
</div>
</section>
{/* Seitenspalte Rechts */}
<div className="space-y-6">
{/* Anstehendes — Erinnerungen & Routinen */}
<section>
<ZoneLabel>Anstehendes</ZoneLabel>
<ErinnerungenCard />
</section>
{/* Werkzeuge & Shortcuts */}
<section>
<ZoneLabel>Werkzeuge & Diagnose</ZoneLabel>
<ToolListCard agent={agent} svcErrors={svcErrors} onNavigate={onNavigate} />
</section>
</div>
</div>
{dialogElement}
</div>
@@ -228,8 +206,6 @@ function ZoneLabel({ children }: { children: React.ReactNode }) {
}
// Ehrliche Status-Zeile: großes Verdikt + Speicher-Pille. Erzählrahmen = Box.
// Bei „warn" wird der ECHTE Hinweis gezeigt (vorher stand hier nur „nur ein Hinweis" —
// ohne zu sagen, welcher; User-Feedback 08.07.).
function StatusLine({ verdict, memory, warns }: {
verdict: ReturnType<typeof useLucyHealth>["verdict"]
memory: ReturnType<typeof useLucyHealth>["memory"]
@@ -249,7 +225,7 @@ function StatusLine({ verdict, memory, warns }: {
const memBar = { ok: "bg-emerald-500", warn: "bg-amber-500", full: "bg-red-500", unknown: "bg-muted-foreground/40" }[memory.status]
return (
<div className={cn("flex flex-col gap-4 rounded-2xl border p-5 shadow-lg shadow-black/15 backdrop-blur-md transition-colors sm:flex-row sm:items-center sm:justify-between", b.ring)}>
<div className={cn("flex flex-col gap-4 rounded-2xl border p-5 shadow-lg shadow-black/15 backdrop-blur-md transition-colors sm:flex-row sm:items-center sm:justify-between h-full", b.ring)}>
<div className="flex items-center gap-3">
<div className="flex h-11 w-11 shrink-0 items-center justify-center rounded-xl border border-border/40 bg-background/30">
<Icon className={cn("h-6 w-6", b.iconCls)} />
@@ -279,7 +255,7 @@ function StatusLine({ verdict, memory, warns }: {
}
// „Braucht dich": kritische Probleme (mit 1-Klick-Reparatur) + bereitliegende Updates
// + offene Vorschläge im Auftragsbuch. Nichts offen → ruhige Bestätigung statt leerer Fläche.
// + offene Vorschläge im Auftragsbuch.
function NeedsYou({
problems, pendingCount, openAuftraege, busy, onRepair, onNavigate,
}: {
@@ -295,10 +271,9 @@ function NeedsYou({
const nothing = problems.length === 0 && !hasUpdates && !hasAuftraege
return (
<section>
<ZoneLabel>Braucht dich</ZoneLabel>
<div className="h-full flex flex-col justify-between">
{nothing ? (
<div className="flex items-center gap-3 rounded-2xl border border-emerald-500/25 bg-emerald-500/5 p-4">
<div className="flex items-center gap-3 rounded-2xl border border-emerald-500/25 bg-emerald-500/5 p-4 h-full">
<Check className="h-5 w-5 shrink-0 text-emerald-400" />
<div>
<p className="text-sm font-semibold text-foreground">Nichts zu tun</p>
@@ -306,7 +281,7 @@ function NeedsYou({
</div>
</div>
) : (
<div className="space-y-2">
<div className="space-y-2 h-full flex flex-col justify-center">
{problems.map((c) => (
<div key={c.id} className="flex flex-col sm:flex-row sm:items-center justify-between gap-3 rounded-2xl border border-red-500/25 bg-red-500/5 p-4">
<div className="flex min-w-0 items-center gap-3 w-full">
@@ -341,7 +316,7 @@ function NeedsYou({
</span>
<div>
<p className="text-sm font-bold text-foreground">{openAuftraege} Vorschl{openAuftraege === 1 ? "ag" : "äge"} im Auftragsbuch</p>
<p className="text-xs text-muted-foreground">Die Box hat etwas gebaut oder sich ausgedacht annehmen oder ablehnen.</p>
<p className="text-xs text-muted-foreground">Annehmen oder ablehnen.</p>
</div>
</div>
<ChevronRight className="h-4 w-4 shrink-0 text-primary/70" />
@@ -358,7 +333,7 @@ function NeedsYou({
</span>
<div>
<p className="text-sm font-bold text-foreground">{pendingCount} Update{pendingCount === 1 ? "" : "s"} bereit</p>
<p className="text-xs text-muted-foreground">Ansehen und entscheiden, ob du es einspielst.</p>
<p className="text-xs text-muted-foreground">Ansehen und entscheiden.</p>
</div>
</div>
<ChevronRight className="h-4 w-4 shrink-0 text-amber-300/70" />
@@ -366,6 +341,90 @@ function NeedsYou({
)}
</div>
)}
</section>
</div>
)
}
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 (
<div className="rounded-2xl border border-border/60 bg-card/30 p-4 space-y-3 shadow-sm">
<div className="divide-y divide-border/20">
{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 (
<button
key={idx}
onClick={t.action}
className="w-full py-2.5 px-1.5 flex items-center justify-between text-left hover:bg-card/45 rounded-lg transition-all group cursor-pointer text-xs"
>
<span className="text-muted-foreground group-hover:text-foreground transition-colors font-medium">
{t.label}
</span>
<div className="flex items-center gap-2">
<span className={cn("text-[10px] font-semibold font-mono",
t.tone === "ok" ? "text-emerald-400" :
t.tone === "warn" ? "text-amber-400" :
t.tone === "alert" ? "text-red-400" : "text-muted-foreground/60"
)}>
{t.status}
</span>
<span className={cn("h-1.5 w-1.5 rounded-full", dotColor)} />
</div>
</button>
)
})}
</div>
</div>
)
}