Feat: Live-Panel aktive Modelle + Fix llama-swap Log-Viewer

- ActiveModelsCard: zeigt geladene Modelle (aus /running via useModels) mit
  Rolle, Größe (Unified-RAM) und warm-Status; globaler "Inferenz aktiv"-Puls
  via useTokenStats-Delta. Prominent oben im Dashboard. Kein Backend-Change.
- maintenance.logs(): journalctl ohne sudo zuerst (User in Gruppe adm darf das
  System-Journal lesen), nur bei fehlenden Rechten sudo-Fallback. Behebt
  "Unbekannter Fehler" beim llama-swap-Log im MC2-UI.

tsc + Build grün; Dashboard rendert die Live-Karte (Leerzustand lokal, da Engine
offline), keine Konsolenfehler.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-06-26 19:21:53 +02:00
parent e2547ec301
commit 93111c6eaf
8 changed files with 499 additions and 391 deletions
@@ -0,0 +1,90 @@
import { useEffect, useRef, useState } from "react"
import { Activity, Zap, Moon } from "lucide-react"
import { useModels, useTokenStats } from "@/lib/queries"
import { fmtSize } from "@/lib/format"
import { cn } from "@/lib/utils"
const ROLE_TONE: Record<string, string> = {
fast: "bg-cyan-500/15 text-cyan-400 border-cyan-500/25",
heavy: "bg-amber-500/15 text-amber-400 border-amber-500/25",
coder: "bg-violet-500/15 text-violet-400 border-violet-500/25",
reasoning: "bg-emerald-500/15 text-emerald-400 border-emerald-500/25",
vision: "bg-pink-500/15 text-pink-400 border-pink-500/25",
scout: "bg-teal-500/15 text-teal-400 border-teal-500/25",
hermes: "bg-indigo-500/15 text-indigo-400 border-indigo-500/25",
}
export function ActiveModelsCard() {
const { data } = useModels(2_000)
const { data: stats } = useTokenStats(2_000)
const models = data?.models ?? []
const running = data?.running ?? []
const loaded = models.filter((m) => running.includes(m.name))
// "Inferenz aktiv": total_tokens ist seit dem letzten Poll gestiegen → es wird generiert.
const prev = useRef<number | null>(null)
const [generating, setGenerating] = useState(false)
useEffect(() => {
if (!stats) return
const total = stats.total_tokens
if (prev.current !== null && total > prev.current) {
setGenerating(true)
const t = setTimeout(() => setGenerating(false), 4_000)
prev.current = total
return () => clearTimeout(t)
}
prev.current = total
}, [stats?.total_tokens])
return (
<div className="rounded-2xl border border-border/60 bg-card/45 backdrop-blur-md p-5 shadow-lg shadow-black/15">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-2">
<Activity className="h-4.5 w-4.5 text-primary" />
<h2 className="text-sm font-semibold tracking-wide uppercase text-foreground">Aktive Modelle (Live)</h2>
</div>
<span className={cn(
"flex items-center gap-1.5 text-[10px] font-bold uppercase tracking-wider font-mono px-2 py-0.5 rounded-full border transition-colors",
generating
? "bg-emerald-500/10 text-emerald-400 border-emerald-500/30"
: "bg-background/30 text-muted-foreground/70 border-border/40"
)}>
{generating ? <Zap className="h-3 w-3 animate-pulse" /> : <Moon className="h-3 w-3" />}
{generating ? "Inferenz aktiv" : "Idle"}
</span>
</div>
{loaded.length === 0 ? (
<div className="flex items-center justify-center gap-2 h-20 text-xs text-muted-foreground/70 italic">
Kein Modell geladen Auto-Swap lädt bei Anfrage.
</div>
) : (
<div className="grid gap-2 sm:grid-cols-2 xl:grid-cols-3">
{loaded.map((m) => (
<div key={m.name} className="flex items-center justify-between gap-2 p-3 rounded-xl bg-emerald-500/5 border border-emerald-500/20">
<div className="min-w-0">
<div className="flex items-center gap-1.5">
{m.role && (
<span className={cn("text-[8px] font-semibold uppercase px-1.5 py-0.5 rounded font-mono border tracking-wider shrink-0",
ROLE_TONE[m.role] || "bg-slate-500/15 text-slate-300 border-slate-500/25")}>
{m.role}
</span>
)}
<span className="text-xs font-semibold font-mono text-foreground truncate">
{m.name.split("/").pop()?.replace(/\.gguf$/i, "")}
</span>
</div>
<div className="text-[9px] font-mono text-muted-foreground/70 mt-0.5">
{fmtSize(m.size_bytes)} im Unified-RAM
</div>
</div>
<span className="flex items-center gap-1 text-[8px] font-semibold text-emerald-400 uppercase tracking-wider font-mono shrink-0">
<span className={cn("h-1.5 w-1.5 rounded-full bg-emerald-500", generating && "animate-pulse")} /> warm
</span>
</div>
))}
</div>
)}
</div>
)
}
+4
View File
@@ -1,3 +1,4 @@
import { ActiveModelsCard } from "@/components/dashboard/ActiveModelsCard"
import { SystemStatusCard } from "@/components/dashboard/SystemStatusCard"
import { UpdatesCard } from "@/components/dashboard/UpdatesCard"
import { AgentStatusCard } from "@/components/dashboard/AgentStatusCard"
@@ -16,6 +17,9 @@ export function DashboardView() {
<p className="text-sm text-muted-foreground">Aktueller Status von System, Modellen und Agent.</p>
</div>
{/* Live: aktuell geladene/arbeitende Modelle */}
<ActiveModelsCard />
{/* Top Grid: System stats & Updates */}
<div className="grid gap-6 md:grid-cols-3">
<SystemStatusCard />