Files
mission-control-v2/frontend/src/views/models/werkbank/MaschinenraumBar.tsx
T
Hitonabi 0c69c491e5 Single Source of Truth: Frontend/Backend-Ungereimtheiten ausgeraeumt (Review 15.07.)
Backend (Wahrheit reparieren):
- gateway_reachable() prueft jetzt den ECHTEN mc2-gateway (:9010 via V1_UPSTREAM)
  statt nur die Engine - toter Denkpfad war in Health/Cockpit unsichtbar
- /api/system/services vollstaendig: + LLM-Gateway, + Steuerpult, + Waechter
  (mc2-steward via is-active); scope-Feld (user/system); mc2-gateway/mc2-steward
  in die Restart-Allowlist
- Connect-Health Leitung 1 prueft den Client-Pfad (V1_UPSTREAM statt llama-swap)
- agent_status liefert pc_executor_url (UI zeigte hartcodierte IP/Ports)
- SSE-Endlosschleife gefixt: /api/auftragsbuch schrieb announce-branches bei JEDEM
  Aufruf neu -> mtime-Bump -> invalidate -> Refetch im 3-s-Takt je Client;
  jetzt nur noch bei echter Aenderung

Frontend (ein Datenweg):
- SystemDrawer komplett auf React-Query-Hooks (vorher eigenes 3-s-setInterval auf
  dieselben Endpunkte inkl. teurem Updates-Check); Dienste-Liste = Backend-Antwort
  (SERVICES-UI-Kopie geloescht); useDialog statt Eigenbau; fmtBytes statt Duplikat
- useLucyHealth: Dienst-Matching per systemd-unit statt Namensfragment; Gateway-
  Reparatur zielt auf mc2-gateway; neuer Waechter-Check; Backend-tot => ehrliches
  rotes Verdikt statt eingefrorener letzter Stand (auch Sidebar)
- Toter Code raus: views/models/Cockpit.tsx (908 Z.) + RoleAssignModal + Placeholder;
  LaneEditor (Routing-Policy) + WarmSetManager damit ZURUECK im UI als Tab
  "Routing & Warm-Set" im Modell-Manager
- Cockpit: blockierte Ideen-Karten erscheinen in "Braucht dich" + Kachel-Hint;
  Verbinden-Kachel zeigt 3 Leitungen live statt hartem "Zed"
- Maschinenraum: ehrliches "frei" (reale Belegung) + eigenes KV-Cache-Segment
- AgentView: Ports/PC-Adresse aus der API; Guide lehrt Lanes chat/coding
- queries.ts: useModels nutzt SSE-relax; Chronik-Limit im Query-Key;
  useJobs/useZeitmaschine mit enabled-Schalter

Verifiziert: tsc+vite gruen, Backend-Smoke beide Gateway-Modi, UI live gegen die
Box (Cockpit/Werkbank/Routing-Tab/Drawer rendern mit Echtdaten).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 22:27:32 +02:00

134 lines
6.3 KiB
TypeScript

import { HardDrive } from "lucide-react"
import type { ModelInfo } from "@/lib/api"
import { fmtSize, gb } from "@/lib/format"
import { roleMeta } from "@/lib/roleMeta"
import { cn } from "@/lib/utils"
import { roleBarColor } from "./roleColors"
const B = 1024 ** 3
const short = (name: string) => name.split("/").pop()?.replace(/\.gguf$/i, "") ?? name
// Maschinenraum-Speicherleiste — das Signatur-Element. EIN großer Balken „Arbeitsspeicher
// · 124 GB", farbig aufgeteilt nach geladenem Modell (nach Rolle eingefärbt) + freier Rest.
// Man sieht LIVE, wohin die GB gehen. Ein Klick auf ein Segment wählt das Modell rechts.
export function MaschinenraumBar({
running, capacityBytes, realUsedBytes, selected, onSelect,
}: {
running: ModelInfo[]
capacityBytes: number
realUsedBytes: number
selected?: string | null
onSelect?: (name: string) => void
}) {
const capacity = capacityBytes > 2 * B ? capacityBytes : 124 * B
const weights = running.reduce((a, m) => a + (m.size_bytes || 0), 0)
// Ehrliche Rechnung: „frei" geht von der REALEN Belegung aus (inkl. KV-Cache), nicht
// nur von den Dateigrößen — vorher konnte die Leiste viel „frei" zeigen, während der
// Speicher praktisch voll war (Review 15.07.). Die Differenz wird als eigenes
// Betriebs-Segment (KV-Cache/Overhead) sichtbar statt verschwiegen.
const overheadBytes = Math.max(0, realUsedBytes - weights)
const freeBytes = Math.max(0, capacity - Math.max(weights, realUsedBytes))
const freePct = (freeBytes / capacity) * 100
const overheadPct = (overheadBytes / capacity) * 100
return (
<div className="mc-card p-5">
<div className="mb-3 flex flex-wrap items-end justify-between gap-2">
<div className="flex items-center gap-2">
<HardDrive className="h-5 w-5 text-primary" />
<div>
<div className="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground">Modell-Speicher (GPU-Pool)</div>
<div className="font-space text-xl font-bold tracking-tight text-foreground">{gb(capacity)} GB <span className="text-sm font-medium text-muted-foreground">gesamt</span></div>
</div>
</div>
<div className="text-right text-[11px] font-mono text-muted-foreground">
<div><span className="font-bold text-foreground">{gb(weights)} GB</span> Modell-Gewichte</div>
{overheadBytes > 0 && <div>{gb(overheadBytes)} GB KV-Cache & Betrieb</div>}
<div className="text-emerald-400">{gb(freeBytes)} GB frei</div>
</div>
</div>
{/* Der Balken */}
<div className="flex h-12 w-full overflow-hidden rounded-xl border border-border/40 bg-background/50 p-1">
{running.length === 0 ? (
<div className="flex w-full items-center justify-center text-[11px] italic text-muted-foreground/60">
Kein Modell geladen Auto-Swap holt bei Anfrage automatisch das passende.
</div>
) : (
<>
{running.map((m) => {
const pct = ((m.size_bytes || 0) / capacity) * 100
const c = roleBarColor(m.role)
const isSel = selected === m.name
return (
<button
key={m.name}
onClick={() => onSelect?.(m.name)}
style={{ width: `${pct}%` }}
title={`${roleMeta(m.role).label} · ${short(m.name)} · ${fmtSize(m.size_bytes)}`}
className={cn(
"group flex h-full min-w-[2.5rem] shrink-0 flex-col justify-center gap-0.5 rounded-lg px-2 text-left text-white transition-all",
c.bar,
isSel ? "ring-2 ring-white/70" : "opacity-90 hover:opacity-100",
)}
>
<span className="truncate font-space text-[10px] font-bold leading-tight tracking-wide">
{roleMeta(m.role).short}
</span>
<span className="truncate font-mono text-[9px] leading-tight opacity-85">{fmtSize(m.size_bytes)}</span>
</button>
)
})}
{overheadPct > 1.5 && (
<div
style={{ width: `${overheadPct}%` }}
className="flex h-full min-w-[2rem] items-center justify-center rounded-lg border border-dashed border-border/60 bg-background/40 text-[9px] font-mono text-muted-foreground/70"
title={`${gb(overheadBytes)} GB KV-Cache & Betrieb (reale Belegung über den Modell-Gewichten)`}
>
KV
</div>
)}
{freePct > 1.5 && (
<div
style={{ width: `${freePct}%` }}
className="flex h-full min-w-[2rem] items-center justify-center rounded-lg text-[9px] font-mono text-muted-foreground/50"
title={`${gb(freeBytes)} GB frei`}
>
frei
</div>
)}
</>
)}
</div>
{/* Legende */}
{running.length > 0 && (
<div className="mt-3 flex flex-wrap gap-x-4 gap-y-1.5">
{running.map((m) => {
const c = roleBarColor(m.role)
return (
<button
key={m.name}
onClick={() => onSelect?.(m.name)}
className="flex items-center gap-1.5 text-[11px] text-muted-foreground transition-colors hover:text-foreground"
>
<span className={cn("h-2.5 w-2.5 rounded-sm", c.dot)} />
<span className={cn("font-semibold", c.text)}>{roleMeta(m.role).short}</span>
<span className="font-mono text-muted-foreground/70">{short(m.name)}</span>
</button>
)
})}
{overheadBytes > 0 && (
<span className="flex items-center gap-1.5 text-[11px] text-muted-foreground/60" title="Reale Belegung über den Modell-Gewichten (Kontext-Speicher der laufenden Modelle)">
<span className="h-2.5 w-2.5 rounded-sm border border-dashed border-border/60 bg-background/40" /> KV-Cache & Betrieb · {gb(overheadBytes)} GB
</span>
)}
<span className="flex items-center gap-1.5 text-[11px] text-muted-foreground/60">
<span className="h-2.5 w-2.5 rounded-sm border border-border/50 bg-background/50" /> frei · {gb(freeBytes)} GB
</span>
</div>
)}
</div>
)
}