Refactor: DashboardView in Karten zerlegen (Phase 4a)

DashboardView 849 -> ~40 Zeilen Orchestrator. Sechs eigenständige Karten unter
components/dashboard/, jede mit eigenem Daten-Hook (react-query, geteilte Keys):
- RadialGauge, SystemStatusCard (useSystemStatus)
- UpdatesCard (useUpdates+useJobs; OS/Engine-Update, Reboot, Modell-Upgrade,
  Sudo-Modal, invalidiert nach Aktionen)
- AgentStatusCard (useAgentStatus+useModels; Gehirn-Wechsel-Modal)
- RolesCard (useModels), MemoryInputCard (useMemory), TokenStatsCard (useTokenStats)

Kein manuelles useEffect+setInterval mehr; useDialog statt lokalem Dialog-State.
Verifiziert: tsc grün, Build grün, alle 6 Karten rendern, Live-Daten + Pricing-
Footer korrekt, keine Konsolenfehler.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-06-26 14:50:54 +02:00
parent 0266dc9e92
commit 7a11fd2846
11 changed files with 1175 additions and 1210 deletions
@@ -0,0 +1,92 @@
import { useState } from "react"
import { Brain, Plus } from "lucide-react"
import { api } from "@/lib/api"
import { useMemory, useQueryClient } from "@/lib/queries"
export function MemoryInputCard() {
const qc = useQueryClient()
const { data: memories = [] } = useMemory({ limit: 3 })
const [memContent, setMemContent] = useState("")
const [memCat, setMemCat] = useState("stable")
const [savingMem, setSavingMem] = useState(false)
async function saveQuickMemory() {
if (!memContent.trim() || savingMem) return
setSavingMem(true)
try {
await api("/api/memory", {
method: "POST",
body: JSON.stringify({ content: memContent, category: memCat, source: "dashboard" }),
})
setMemContent("")
qc.invalidateQueries({ queryKey: ["memory"] })
} catch (e) {
console.error(e)
} finally {
setSavingMem(false)
}
}
return (
<div className="rounded-2xl border border-border/60 bg-card/45 backdrop-blur-md p-5 shadow-lg shadow-black/15 flex flex-col justify-between">
<div>
<div className="flex items-center gap-2 mb-4">
<Brain className="h-4.5 w-4.5 text-primary" />
<h2 className="text-sm font-semibold tracking-wide uppercase text-foreground">Gedächtnis</h2>
</div>
<div className="space-y-3">
<textarea
value={memContent}
onChange={(e) => setMemContent(e.target.value)}
placeholder="Fakt / Regel im Pool speichern..."
rows={2}
className="w-full resize-none rounded-xl border border-border/50 bg-background/30 p-2 text-[10px] outline-none focus:ring-1 focus:ring-primary transition-all leading-normal"
/>
<div className="flex items-center gap-2 justify-between">
<select
value={memCat}
onChange={(e) => setMemCat(e.target.value)}
className="h-7 rounded-lg border border-border/50 bg-background/50 px-2 text-[10px] outline-none cursor-pointer"
>
<option value="stable">🔵 Fakt</option>
<option value="instruction">📋 Regel</option>
<option value="user">👤 User</option>
<option value="versioned">🟡 Version</option>
</select>
<button
onClick={saveQuickMemory}
disabled={!memContent.trim() || savingMem}
className="flex h-7 items-center gap-1 rounded-lg bg-primary px-3 text-[10px] font-semibold text-primary-foreground hover:opacity-90 disabled:opacity-50 transition-all cursor-pointer"
>
<Plus className="h-3.5 w-3.5" /> Speichern
</button>
</div>
</div>
<div className="mt-3.5 space-y-1.5">
<div className="text-[9px] text-muted-foreground uppercase font-bold tracking-wider">Zuletzt gespeichert:</div>
<div className="max-h-20 overflow-y-auto space-y-1 pr-1 scrollbar-thin">
{memories.length === 0 ? (
<div className="text-[10px] text-muted-foreground/75 py-1">Keine Einträge vorhanden.</div>
) : (
memories.map((m) => (
<div key={m.id} className="text-[10px] bg-background/10 border border-border/30 rounded p-1.5 flex items-start gap-1.5">
<span className="shrink-0 text-[8px] font-mono text-muted-foreground/80 px-1 py-0.5 rounded bg-muted/20">
{m.category}
</span>
<span className="truncate flex-1 text-muted-foreground hover:text-foreground transition-colors" title={m.content}>
{m.content}
</span>
</div>
))
)}
</div>
</div>
</div>
<div className="mt-4 text-[10px] text-muted-foreground/80 border-t border-border/30 pt-3">
Steht allen Clients per MCP zur Verfügung.
</div>
</div>
)
}