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:
@@ -167,17 +167,21 @@ def restart_service(name: str, sudo_password: str | None = None) -> dict:
|
||||
|
||||
def logs(service: str, lines: int = 200, sudo_password: str | None = None) -> dict:
|
||||
lines = max(1, min(lines, 1000))
|
||||
if service in USER_SERVICES:
|
||||
r = _run(["journalctl", "--user", "-u", service, "-n", str(lines), "--no-pager"])
|
||||
return {"ok": r["ok"], "text": r["out"] or r["err"]}
|
||||
if service in SYSTEM_SERVICES:
|
||||
# Journal-Lesen braucht i.d.R. KEIN sudo (User ist in Gruppe adm/systemd-journal).
|
||||
# Erst ohne sudo versuchen; nur bei fehlenden Rechten auf sudo zurückfallen.
|
||||
r = _run(["journalctl", "-u", service, "-n", str(lines), "--no-pager"])
|
||||
if r["ok"]:
|
||||
return {"ok": True, "text": r["out"] or "(keine Log-Einträge)"}
|
||||
if err := check_sudo_needs_password(sudo_password):
|
||||
return err
|
||||
cmd = ["sudo", "journalctl", "-u", service, "-n", str(lines), "--no-pager"]
|
||||
r = _run(cmd, sudo_password=sudo_password)
|
||||
elif service in USER_SERVICES:
|
||||
cmd = ["journalctl", "--user", "-u", service, "-n", str(lines), "--no-pager"]
|
||||
r = _run(cmd)
|
||||
else:
|
||||
return {"ok": False, "text": "", "err": "Dienst nicht erlaubt."}
|
||||
r = _run(["sudo", "journalctl", "-u", service, "-n", str(lines), "--no-pager"],
|
||||
sudo_password=sudo_password)
|
||||
return {"ok": r["ok"], "text": r["out"] or r["err"]}
|
||||
return {"ok": False, "text": "", "err": "Dienst nicht erlaubt."}
|
||||
|
||||
def check_updates_job(sudo_password: str | None = None) -> dict:
|
||||
if err := check_sudo_needs_password(sudo_password):
|
||||
|
||||
+1
File diff suppressed because one or more lines are too long
-1
File diff suppressed because one or more lines are too long
+390
File diff suppressed because one or more lines are too long
-380
File diff suppressed because one or more lines are too long
Vendored
+2
-2
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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 />
|
||||
|
||||
Reference in New Issue
Block a user