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
+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 },
]
+125 -52
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..."
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"
/>
</div>
<div className="flex items-center gap-2 justify-end">
<textarea
value={memContent}
onChange={(e) => setMemContent(e.target.value)}
placeholder="Fakt / Regel im Pool speichern..."
rows={2}
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 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,33 +353,35 @@ 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>
{memories.length === 0 ? (
<div className="text-xs text-muted-foreground/75 py-2">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">
{m.category}
</span>
<span className="truncate flex-1 text-muted-foreground hover:text-foreground transition-colors" title={m.content}>
{m.content}
</span>
</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-[10px] text-muted-foreground/75 py-1">Keine Einträge vorhanden.</div>
) : (
memories.map((m) => (
<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}>
{m.content}
</span>
</div>
))
)}
</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>
)
}