feat: consolidate routing view into models tab, and add updates widget directly to Zentrale dashboard

This commit is contained in:
Hitonabi
2026-06-25 22:24:00 +02:00
parent 6a8e55cc43
commit d15744a812
9 changed files with 481 additions and 402 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
@@ -6,8 +6,8 @@
<meta name="theme-color" content="#0d1117" />
<link rel="manifest" href="/manifest.webmanifest" />
<title>Mission Control 2.0</title>
<script type="module" crossorigin src="/assets/index-DpycPcE0.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-GX6Y3U0B.css">
<script type="module" crossorigin src="/assets/index-CmIKN_Pq.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-D5kA4UxS.css">
</head>
<body>
<div id="root"></div>
+8 -3
View File
@@ -4,7 +4,6 @@ import { NAV, type ViewId } from "@/nav"
import { CommandPalette } from "@/components/CommandPalette"
import { DashboardView } from "@/views/DashboardView"
import { ModelsView } from "@/views/ModelsView"
import { RoutingView } from "@/views/RoutingView"
import { SystemView } from "@/views/SystemView"
import { ConnectView } from "@/views/ConnectView"
import { MemoryView } from "@/views/MemoryView"
@@ -41,6 +40,12 @@ export default function App() {
localStorage.setItem("mc_theme", dark ? "dark" : "light")
}, [dark])
useEffect(() => {
const handleOpen = () => setDrawerOpen(true)
window.addEventListener("open-system-drawer", handleOpen)
return () => window.removeEventListener("open-system-drawer", handleOpen)
}, [])
const active = NAV.find((n) => n.id === view)!
const totalUpdates = updates ? (updates.os + updates.engine + updates.models) : 0
@@ -182,17 +187,17 @@ export default function App() {
<main className="flex-1 overflow-y-auto p-6 scrollbar-thin">
{view === "dashboard" && <DashboardView />}
{view === "models" && <ModelsView />}
{view === "routing" && <RoutingView />}
{view === "system" && <SystemView />}
{view === "connect" && <ConnectView />}
{view === "memory" && <MemoryView />}
{view === "agent" && <AgentView />}
{!["dashboard", "models", "routing", "system", "connect", "memory", "agent"].includes(view) && (
{!["dashboard", "models", "system", "connect", "memory", "agent"].includes(view) && (
<Placeholder title={active.label} hint={active.hint} />
)}
</main>
</div>
</div>
)
}
+3 -4
View File
@@ -1,7 +1,6 @@
import {
LayoutDashboard,
Boxes,
Route,
Cpu,
Brain,
Plug,
@@ -9,7 +8,7 @@ import {
type LucideIcon,
} from "lucide-react"
export type ViewId = "dashboard" | "models" | "routing" | "system" | "memory" | "connect" | "agent"
export type ViewId = "dashboard" | "models" | "system" | "memory" | "connect" | "agent"
export interface NavItem {
id: ViewId
@@ -21,10 +20,10 @@ export interface NavItem {
// Eine Quelle der Wahrheit für Sidebar UND Command-Palette.
export const NAV: NavItem[] = [
{ id: "dashboard", label: "Zentrale", hint: "System- & Stack-Status", icon: LayoutDashboard },
{ id: "models", label: "Modelle & Routing", hint: "Modelle kuratieren, Gruppen & Auto-Routing", icon: Boxes },
{ id: "routing", label: "Routing", hint: "Gateway-Regeln: schnell ↔ schwer", icon: Route },
{ id: "models", label: "Modell-Zentrale", hint: "Verwalten, laden & Gateway-Routing", icon: Boxes },
{ id: "system", label: "Diagnose", hint: "Metriken, Dienste, Logs", icon: Cpu },
{ id: "memory", label: "Gedächtnis", hint: "Geteiltes Memory verwalten", icon: Brain },
{ id: "connect", label: "Verbinden", hint: "IDE-/Agent-Configs erzeugen", icon: Plug },
{ id: "agent", label: "Hermes", hint: "Agent-Status & WebUI öffnen", icon: Bot },
]
+109 -36
View File
@@ -1,6 +1,6 @@
import { useEffect, useState } from "react"
import { Bot, Cpu, ExternalLink, Brain, Layers, Plus } from "lucide-react"
import { api, type SystemStatus, type AgentStatus, type ModelInfo, type Memory } from "@/lib/api"
import { Bot, Cpu, ExternalLink, Brain, Layers, Plus, ShieldAlert } from "lucide-react"
import { api, type SystemStatus, type AgentStatus, type ModelInfo, type Memory, type UpdatesResp } from "@/lib/api"
import { cn } from "@/lib/utils"
function gb(b: number) {
@@ -12,7 +12,6 @@ function RadialGauge({ value, label, detail }: { value: number; label: string; d
const circ = 2 * Math.PI * radius
const offset = circ - (Math.min(value, 100) / 100) * circ
// Verfärbung bei hoher Last
const strokeColor = value > 90
? "stroke-red-500"
: value > 75
@@ -40,6 +39,7 @@ export function DashboardView() {
const [models, setModels] = useState<ModelInfo[]>([])
const [running, setRunning] = useState<string[]>([])
const [memories, setMemories] = useState<Memory[]>([])
const [updates, setUpdates] = useState<UpdatesResp | null>(null)
// Quick Memory Form State
const [memContent, setMemContent] = useState("")
@@ -51,11 +51,12 @@ export function DashboardView() {
api<AgentStatus>("/api/agent/status").then(setAgent).catch(() => {})
api<{ models: ModelInfo[]; running?: string[] }>("/api/models")
.then((d) => {
setModels(d.models)
setModels(d.models || [])
setRunning(d.running || [])
})
.catch(() => {})
api<Memory[]>("/api/memory?category=").then((d) => setMemories(d.slice(0, 3))).catch(() => {})
api<UpdatesResp>("/api/maintenance/updates").then(setUpdates).catch(() => {})
}
useEffect(() => {
@@ -73,7 +74,6 @@ export function DashboardView() {
body: JSON.stringify({ content: memContent, category: memCat, source: "dashboard" }),
})
setMemContent("")
// Liste sofort aktualisieren
api<Memory[]>("/api/memory?category=").then((d) => setMemories(d.slice(0, 3))).catch(() => {})
} catch (e) {
console.error(e)
@@ -86,6 +86,7 @@ export function DashboardView() {
return (
<div className="space-y-6">
{/* Title */}
<div>
<h1 className="text-2xl font-space font-bold tracking-tight bg-gradient-to-r from-foreground via-foreground to-primary bg-clip-text text-transparent">
Zentrale
@@ -93,9 +94,10 @@ export function DashboardView() {
<p className="text-sm text-muted-foreground">Aktueller Status von System, Modellen und Agent.</p>
</div>
<div className="grid gap-6 sm:grid-cols-2">
{/* Card 1: System Status */}
<div className="flex flex-col justify-between rounded-2xl border border-border/60 bg-card/45 backdrop-blur-md p-5 shadow-lg shadow-black/15">
{/* Top Grid: System stats & Updates (3 Columns) */}
<div className="grid gap-6 md:grid-cols-3">
{/* Card 1: System Status (2 columns wide) */}
<div className="md:col-span-2 flex flex-col justify-between rounded-2xl border border-border/60 bg-card/45 backdrop-blur-md p-5 shadow-lg shadow-black/15">
<div>
<div className="flex items-center gap-2 mb-4">
<Cpu className="h-4.5 w-4.5 text-primary" />
@@ -105,11 +107,11 @@ export function DashboardView() {
<div className="grid grid-cols-2 gap-3 sm:grid-cols-4">
<RadialGauge value={sys.cpu.percent} label="CPU" detail={`${sys.cpu.cores} Cores`} />
<RadialGauge value={sys.ram.percent} label="RAM" detail={`${gb(sys.ram.used)} / ${gb(sys.ram.total)} GB`} />
{sys.gpu && sys.gpu.busy_percent != null && (
{sys.gpu && sys.gpu.busy_percent != null && sys.gpu.gtt_used != null && sys.gpu.gtt_total != null && (
<RadialGauge
value={sys.gpu.busy_percent}
label="GPU"
detail={sys.gpu.gtt_used != null && sys.gpu.gtt_total != null ? `${gb(sys.gpu.gtt_used)} / ${gb(sys.gpu.gtt_total)} GB` : undefined}
detail={`${gb(sys.gpu.gtt_used)} / ${gb(sys.gpu.gtt_total)} GB`}
/>
)}
{sys.disk && (
@@ -128,7 +130,80 @@ export function DashboardView() {
)}
</div>
{/* Card 2: Hermes Agent Status */}
{/* Card 2: Updates & Wartung (1 column wide) */}
<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">
<ShieldAlert className="h-4.5 w-4.5 text-primary" />
<h2 className="text-sm font-semibold tracking-wide uppercase text-foreground">Updates &amp; Pflege</h2>
</div>
{updates ? (
<div className="space-y-2.5">
<div className="space-y-1.5">
<div className={cn(
"flex items-center justify-between px-3 py-1.5 rounded-lg border text-xs",
updates.os > 0
? "border-amber-500/30 bg-amber-500/5 text-amber-400 font-semibold"
: "border-border/30 bg-background/25 text-muted-foreground"
)}>
<span>OS-Pakete</span>
<span className="font-mono">{updates.os > 0 ? `${updates.os} verfügbar` : "aktuell"}</span>
</div>
<div className={cn(
"flex items-center justify-between px-3 py-1.5 rounded-lg border text-xs",
updates.engine > 0
? "border-amber-500/30 bg-amber-500/5 text-amber-400 font-semibold"
: "border-border/30 bg-background/25 text-muted-foreground"
)}>
<span>Engine (llama.cpp)</span>
<span className="font-mono">{updates.engine > 0 ? "Update verfügbar" : "aktuell"}</span>
</div>
<div className={cn(
"flex items-center justify-between px-3 py-1.5 rounded-lg border text-xs",
updates.models > 0
? "border-primary/30 bg-primary/5 text-primary font-semibold animate-pulse"
: "border-border/30 bg-background/25 text-muted-foreground"
)}>
<span>Modell-Upgrades</span>
<span className="font-mono">{updates.models > 0 ? `${updates.models} verfügbar` : "aktuell"}</span>
</div>
</div>
{updates.model_list.length > 0 && (
<div className="mt-2 space-y-1">
<div className="text-[9px] uppercase font-bold text-muted-foreground/60 tracking-wider">Upgrades:</div>
<div className="max-h-20 overflow-y-auto space-y-1 pr-1 scrollbar-thin">
{updates.model_list.map((m) => (
<div key={m.repo} className="text-[9px] bg-background/20 border border-border/20 rounded p-1.5 font-mono text-muted-foreground truncate" title={`${m.role}: ${m.repo}`}>
<span className="text-primary font-semibold uppercase">{m.role}</span>: {m.repo.split("/").pop()}
</div>
))}
</div>
</div>
)}
</div>
) : (
<div className="h-24 flex items-center justify-center text-xs text-muted-foreground">Lade Updates...</div>
)}
</div>
<div className="mt-4 border-t border-border/30 pt-3 shrink-0">
<button
onClick={() => window.dispatchEvent(new CustomEvent("open-system-drawer"))}
className="w-full h-8 flex items-center justify-center gap-1.5 rounded-lg bg-primary text-primary-foreground text-xs font-semibold hover:opacity-90 transition-all cursor-pointer shadow-md shadow-primary/10"
>
System-Zentrale öffnen
</button>
</div>
</div>
</div>
{/* Bottom Grid: Agent, Roles & Memory (3 Columns) */}
<div className="grid gap-6 md:grid-cols-3">
{/* Card 3: Hermes Agent Status */}
<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 justify-between mb-4">
@@ -188,10 +263,8 @@ export function DashboardView() {
Gedächtnis & Stack-Tools via MCP gekoppelt.
</div>
</div>
</div>
<div className="grid gap-6 sm:grid-cols-2">
{/* Card 3: Active Model Roles */}
{/* Card 4: Active Model Roles */}
<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">
@@ -199,7 +272,7 @@ export function DashboardView() {
<h2 className="text-sm font-semibold tracking-wide uppercase text-foreground">Aktive Rollen</h2>
</div>
<div className="space-y-2">
<div className="space-y-2 max-h-48 overflow-y-auto pr-1 scrollbar-thin">
{activeModels.length === 0 ? (
<div className="text-xs text-muted-foreground py-6 text-center">Keine Modelle als Rollen zugewiesen.</div>
) : (
@@ -209,15 +282,15 @@ export function DashboardView() {
<div
key={m.name}
className={cn(
"flex items-center justify-between p-3 rounded-xl border transition-all duration-300",
"flex items-center justify-between p-2.5 rounded-xl border transition-all duration-300",
isRunning
? "border-primary/50 bg-primary/5 shadow-md shadow-primary/5"
: "border-border/40 bg-background/20"
)}
>
<div>
<div className="min-w-0 flex-1 mr-2">
<div className="flex items-center gap-2">
<span className={cn("text-xs font-semibold uppercase px-1.5 py-0.5 rounded",
<span className={cn("text-[9px] font-semibold uppercase px-1.5 py-0.5 rounded font-mono",
m.role === "fast" ? "bg-cyan-500/15 text-cyan-400" :
m.role === "heavy" ? "bg-amber-500/15 text-amber-400" :
m.role === "coder" ? "bg-violet-500/15 text-violet-400" :
@@ -232,12 +305,12 @@ export function DashboardView() {
</span>
)}
</div>
<div className="text-xs font-medium mt-1.5 truncate max-w-[200px] sm:max-w-[280px]" title={m.name}>
<div className="text-[10px] font-medium mt-1 truncate" title={m.name}>
{m.name}
</div>
</div>
<div className="text-[10px] font-mono text-muted-foreground">
{isRunning ? "Warm / Aktiv" : "Bereit"}
<div className="text-[9px] font-mono text-muted-foreground shrink-0">
{isRunning ? "Warm" : "Bereit"}
</div>
</div>
)
@@ -250,29 +323,27 @@ export function DashboardView() {
</div>
</div>
{/* Card 4: Quick Memory Input */}
{/* Card 5: Quick Memory Input */}
<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-Schnellform</h2>
<h2 className="text-sm font-semibold tracking-wide uppercase text-foreground">Gedächtnis</h2>
</div>
<div className="space-y-3">
<div className="flex items-start gap-2">
<textarea
value={memContent}
onChange={(e) => setMemContent(e.target.value)}
placeholder="Fakt / Regel auf der Box speichern..."
placeholder="Fakt / Regel im Pool speichern..."
rows={2}
className="flex-1 resize-none rounded-xl border border-border/50 bg-background/30 px-3 py-2 text-xs outline-none focus:ring-1.5 focus:ring-primary transition-all"
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>
<div className="flex items-center gap-2 justify-end">
<div className="flex items-center gap-2 justify-between">
<select
value={memCat}
onChange={(e) => setMemCat(e.target.value)}
className="rounded-lg border border-border/50 bg-background/50 px-2 py-1 text-xs outline-none"
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>
@@ -282,21 +353,22 @@ export function DashboardView() {
<button
onClick={saveQuickMemory}
disabled={!memContent.trim() || savingMem}
className="flex items-center gap-1 rounded-lg bg-primary px-3 py-1 text-xs font-semibold text-primary-foreground hover:opacity-90 disabled:opacity-50 transition-all cursor-pointer"
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-4 space-y-2">
<div className="text-[10px] text-muted-foreground uppercase font-semibold tracking-wider">Zuletzt gespeichert:</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-xs text-muted-foreground/75 py-2">Keine Einträge vorhanden.</div>
<div className="text-[10px] text-muted-foreground/75 py-1">Keine Einträge vorhanden.</div>
) : (
memories.map((m) => (
<div key={m.id} className="text-xs bg-background/10 border border-border/30 rounded-lg p-2 flex items-start gap-2">
<span className="shrink-0 text-[10px] font-mono text-muted-foreground/80 px-1 py-0.5 rounded bg-muted/20">
<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}>
@@ -307,8 +379,9 @@ export function DashboardView() {
)}
</div>
</div>
</div>
<div className="mt-4 text-[10px] text-muted-foreground/80 border-t border-border/30 pt-3">
Steht allen Clients (IDEs, Hermes) per MCP zur Verfügung.
Steht allen Clients per MCP zur Verfügung.
</div>
</div>
</div>
+7 -5
View File
@@ -2,6 +2,7 @@ import { useEffect, useState } from "react"
import { Download, Search, Trash2, Edit3, Star, Layers, Activity, HardDrive } from "lucide-react"
import { api, type DiscoverResp, type Fit, type Job, type ModelInfo } from "@/lib/api"
import { CapsChips } from "@/components/CapsChips"
import { RoutingView } from "./RoutingView"
import { cn } from "@/lib/utils"
function fmtBytes(b?: number) {
@@ -492,7 +493,7 @@ function Discover() {
}
export function ModelsView() {
const [tab, setTab] = useState<"installed" | "discover">("installed")
const [tab, setTab] = useState<"installed" | "discover" | "routing">("installed")
return (
<div className="space-y-6">
<div className="flex flex-col sm:flex-row justify-between sm:items-center gap-4">
@@ -501,12 +502,12 @@ export function ModelsView() {
Modell-Zentrale
</h1>
<p className="text-sm text-muted-foreground">
Verwalte installierte GGUFs, weise Systemrollen zu und lade neue Modelle von HuggingFace.
Verwalte installierte GGUFs, weise Systemrollen zu, lade neue Modelle oder konfiguriere das Gateway-Routing.
</p>
</div>
<div className="flex rounded-xl border border-border/60 bg-card/45 backdrop-blur-md p-1 self-start">
{(["installed", "discover"] as const).map((t) => (
{(["installed", "discover", "routing"] as const).map((t) => (
<button
key={t}
onClick={() => setTab(t)}
@@ -517,7 +518,7 @@ export function ModelsView() {
: "text-muted-foreground hover:text-foreground",
)}
>
{t === "installed" ? "Installiert" : "Suchen & Entdecken"}
{t === "installed" ? "Bibliothek" : t === "discover" ? "Modelle finden" : "Gateway-Routing"}
</button>
))}
</div>
@@ -526,8 +527,9 @@ export function ModelsView() {
<JobsBar />
<div className="transition-all duration-300">
{tab === "installed" ? <Installed /> : <Discover />}
{tab === "installed" ? <Installed /> : tab === "discover" ? <Discover /> : <RoutingView />}
</div>
</div>
)
}