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
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -7,8 +7,8 @@
<link rel="manifest" href="/manifest.webmanifest" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<title>Mission Control 2.0</title>
<script type="module" crossorigin src="/assets/index-paq9nNtl.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DiSNgbNY.css">
<script type="module" crossorigin src="/assets/index-l7I88DA-.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-1UOWeoZj.css">
</head>
<body>
<div id="root"></div>
@@ -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 />