Feat: Live-Charts auf Diagnose + Token-Durchsatz-Performance-Karte (Recharts)
Gemeinsame LiveAreaChart-Komponente + useSystemHistory-Hook (EINE Quelle der Wahrheit fuer den Live-Verlauf), genutzt von Zentrale & Diagnose. - Diagnose: die 4 statischen Balken (CPU/RAM/GPU/Disk) sind jetzt farbcodierte Einzel-Live-Charts (Spline, Gradient, Hover-Tooltip, dyn. Y-Achse). - Neue TokenPerformanceCard (Zentrale): Live-Durchsatz tok/s, aus den kumulativen Token-Zaehlern als Rate abgeleitet (Prompt/Prefill vs. Antwort/Generierung), KPI- Headline (tok/s, Gesamt-Tokens, gespart EUR), dunkler Performance-Stil. - SystemStatusCard auf die geteilte Komponente/Hook umgestellt (schlanker). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
import { Area, AreaChart, CartesianGrid, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts"
|
||||
|
||||
export type ChartSeries = { key: string; label: string; color: string }
|
||||
|
||||
const GRID = "rgba(130,130,150,0.14)"
|
||||
const AXIS = "rgba(130,130,150,0.85)"
|
||||
|
||||
function niceCeil(v: number): number {
|
||||
if (v <= 0) return 10
|
||||
const mag = Math.pow(10, Math.floor(Math.log10(v)))
|
||||
const n = v / mag
|
||||
const step = n <= 1 ? 1 : n <= 2 ? 2 : n <= 5 ? 5 : 10
|
||||
return step * mag
|
||||
}
|
||||
|
||||
function fmt(v: number, unit: string): string {
|
||||
const n = unit === "%" ? Math.round(v) : v >= 1000 ? `${(v / 1000).toFixed(1)}k` : Math.round(v).toString()
|
||||
return `${n}${unit}`
|
||||
}
|
||||
|
||||
function ChartTooltip({ active, payload, unit }: any) {
|
||||
if (!active || !payload?.length) return null
|
||||
return (
|
||||
<div className="rounded-lg border border-border/70 bg-popover/95 px-3 py-2 shadow-xl backdrop-blur">
|
||||
<div className="space-y-1">
|
||||
{payload.map((p: any) => (
|
||||
<div key={p.dataKey} className="flex items-center gap-2 text-[11px] font-mono">
|
||||
<span className="h-2 w-2 rounded-full" style={{ background: p.color }} />
|
||||
<span className="uppercase tracking-wider text-muted-foreground">{p.name}</span>
|
||||
<span className="ml-auto pl-3 font-bold tabular-nums text-foreground">{fmt(p.value, unit)}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/** Wiederverwendbarer Live-Verlaufsgraph (Recharts Area, glatte Splines, Gradient-Fill,
|
||||
* Hover-Tooltip). yMode='percent' → 0..100 in 25er-Schritten; 'auto' → dynamisch (nice). */
|
||||
export function LiveAreaChart({
|
||||
data, series, unit = "%", yMode = "percent", height = 176,
|
||||
}: {
|
||||
data: any[]
|
||||
series: ChartSeries[]
|
||||
unit?: string
|
||||
yMode?: "percent" | "auto"
|
||||
height?: number
|
||||
}) {
|
||||
const peak = data.reduce(
|
||||
(m, row) => series.reduce((mm, s) => Math.max(mm, Number(row[s.key]) || 0), m), 0
|
||||
)
|
||||
const yMax = yMode === "percent"
|
||||
? Math.min(100, Math.max(25, Math.ceil((peak * 1.2) / 25) * 25))
|
||||
: Math.max(niceCeil(peak * 1.15), 10)
|
||||
|
||||
return (
|
||||
<div style={{ height }} className="w-full">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<AreaChart data={data} margin={{ top: 8, right: 6, bottom: 0, left: -12 }}>
|
||||
<defs>
|
||||
{series.map((s) => (
|
||||
<linearGradient key={s.key} id={`grad-${s.key}`} x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stopColor={s.color} stopOpacity={0.22} />
|
||||
<stop offset="100%" stopColor={s.color} stopOpacity={0} />
|
||||
</linearGradient>
|
||||
))}
|
||||
</defs>
|
||||
<CartesianGrid vertical={false} stroke={GRID} />
|
||||
<XAxis dataKey="t" hide />
|
||||
<YAxis
|
||||
domain={[0, yMax]} ticks={[0, yMax / 2, yMax]} tickFormatter={(v) => fmt(v, unit)}
|
||||
width={42} axisLine={false} tickLine={false} tick={{ fontSize: 10, fill: AXIS }}
|
||||
/>
|
||||
<Tooltip content={<ChartTooltip unit={unit} />} cursor={{ stroke: AXIS, strokeOpacity: 0.4, strokeDasharray: "3 3" }} />
|
||||
{series.map((s) => (
|
||||
<Area
|
||||
key={s.key} type="monotone" dataKey={s.key} name={s.label}
|
||||
stroke={s.color} strokeWidth={2} fill={`url(#grad-${s.key})`}
|
||||
dot={false} activeDot={{ r: 3, strokeWidth: 0 }}
|
||||
isAnimationActive={false} connectNulls
|
||||
/>
|
||||
))}
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,68 +1,21 @@
|
||||
import { useEffect, useState } from "react"
|
||||
import { Cpu } from "lucide-react"
|
||||
import { Area, AreaChart, CartesianGrid, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts"
|
||||
import { useSystemStatus } from "@/lib/queries"
|
||||
import { useSystemHistory } from "@/lib/useSystemHistory"
|
||||
import { gb } from "@/lib/format"
|
||||
import { LiveAreaChart, type ChartSeries } from "./LiveAreaChart"
|
||||
|
||||
const MAX_POINTS = 40 // bei 3s-Poll ~2 Min Live-Verlauf
|
||||
|
||||
type Sample = { t: number; cpu: number; ram: number; gpu: number | null; disk: number | null }
|
||||
|
||||
const SERIES = [
|
||||
const SERIES: ChartSeries[] = [
|
||||
{ key: "cpu", label: "CPU", color: "#2dd4bf" },
|
||||
{ key: "ram", label: "RAM", color: "#38bdf8" },
|
||||
{ key: "gpu", label: "GPU", color: "#a78bfa" },
|
||||
{ key: "disk", label: "Disk", color: "#fbbf24" },
|
||||
] as const
|
||||
|
||||
const GRID = "rgba(130,130,150,0.14)"
|
||||
const AXIS = "rgba(130,130,150,0.85)"
|
||||
|
||||
function ChartTooltip({ active, payload }: any) {
|
||||
if (!active || !payload?.length) return null
|
||||
return (
|
||||
<div className="rounded-lg border border-border/70 bg-popover/95 px-3 py-2 shadow-xl backdrop-blur">
|
||||
<div className="space-y-1">
|
||||
{payload.map((p: any) => (
|
||||
<div key={p.dataKey} className="flex items-center gap-2 text-[11px] font-mono">
|
||||
<span className="h-2 w-2 rounded-full" style={{ background: p.color }} />
|
||||
<span className="uppercase tracking-wider text-muted-foreground">{p.name}</span>
|
||||
<span className="ml-auto pl-3 font-bold tabular-nums text-foreground">{Math.round(p.value)}%</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
]
|
||||
|
||||
export function SystemStatusCard() {
|
||||
const { data: sys, dataUpdatedAt } = useSystemStatus(3_000)
|
||||
const [hist, setHist] = useState<Sample[]>([])
|
||||
|
||||
// Bei jedem Poll (dataUpdatedAt ändert sich pro Fetch) einen Live-Messpunkt anhängen.
|
||||
useEffect(() => {
|
||||
if (!sys) return
|
||||
setHist((h) => [
|
||||
...h,
|
||||
{
|
||||
t: Date.now(),
|
||||
cpu: sys.cpu?.percent ?? 0,
|
||||
ram: sys.ram?.percent ?? 0,
|
||||
gpu: sys.gpu?.busy_percent ?? null,
|
||||
disk: sys.disk?.percent ?? null,
|
||||
},
|
||||
].slice(-MAX_POINTS))
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [dataUpdatedAt])
|
||||
const { sys, hist } = useSystemHistory()
|
||||
|
||||
const hasGpu = !!(sys?.gpu && sys.gpu.busy_percent != null && sys.gpu.gtt_used != null && sys.gpu.gtt_total != null)
|
||||
const activeSeries = SERIES.filter((s) => s.key !== "gpu" || hasGpu)
|
||||
|
||||
// Dynamische Y-Achse: skaliert auf den jüngsten Peak (in 25er-Schritten), damit die
|
||||
// Linien auch bei idle-Box (<20%) den Chart ausfüllen statt unten zu kleben.
|
||||
const peak = hist.reduce((m, s) => Math.max(m, s.cpu, s.ram, s.gpu ?? 0, s.disk ?? 0), 0)
|
||||
const yMax = Math.min(100, Math.max(25, Math.ceil((peak * 1.2) / 25) * 25))
|
||||
|
||||
const current: Record<string, number | null | undefined> = {
|
||||
cpu: sys?.cpu?.percent, ram: sys?.ram?.percent,
|
||||
gpu: hasGpu ? sys!.gpu!.busy_percent : null, disk: sys?.disk?.percent,
|
||||
@@ -87,7 +40,6 @@ export function SystemStatusCard() {
|
||||
|
||||
{sys ? (
|
||||
<>
|
||||
{/* Legende mit Live-Werten */}
|
||||
<div className="mb-1 flex flex-wrap gap-x-5 gap-y-1.5">
|
||||
{activeSeries.map((s) => (
|
||||
<div key={s.key} className="flex items-center gap-1.5">
|
||||
@@ -98,37 +50,7 @@ export function SystemStatusCard() {
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="h-44 w-full">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<AreaChart data={hist} margin={{ top: 8, right: 6, bottom: 0, left: -18 }}>
|
||||
<defs>
|
||||
{SERIES.map((s) => (
|
||||
<linearGradient key={s.key} id={`grad-${s.key}`} x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stopColor={s.color} stopOpacity={0.22} />
|
||||
<stop offset="100%" stopColor={s.color} stopOpacity={0} />
|
||||
</linearGradient>
|
||||
))}
|
||||
</defs>
|
||||
<CartesianGrid vertical={false} stroke={GRID} />
|
||||
<XAxis dataKey="t" hide />
|
||||
<YAxis
|
||||
domain={[0, yMax]} ticks={[0, yMax / 2, yMax]} tickFormatter={(v) => `${v}%`}
|
||||
width={34} axisLine={false} tickLine={false}
|
||||
tick={{ fontSize: 10, fill: AXIS }}
|
||||
/>
|
||||
<Tooltip content={<ChartTooltip />} cursor={{ stroke: AXIS, strokeOpacity: 0.4, strokeDasharray: "3 3" }} />
|
||||
{activeSeries.map((s) => (
|
||||
<Area
|
||||
key={s.key} type="monotone" dataKey={s.key} name={s.label}
|
||||
stroke={s.color} strokeWidth={2} fill={`url(#grad-${s.key})`}
|
||||
dot={false} activeDot={{ r: 3, strokeWidth: 0 }}
|
||||
isAnimationActive={false} connectNulls
|
||||
/>
|
||||
))}
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
<LiveAreaChart data={hist} series={activeSeries} unit="%" yMode="percent" height={176} />
|
||||
</>
|
||||
) : (
|
||||
<div className="flex h-44 items-center justify-center text-xs text-muted-foreground">Lade Systemdaten…</div>
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
import { useEffect, useRef, useState } from "react"
|
||||
import { Activity } from "lucide-react"
|
||||
import { useTokenStats } from "@/lib/queries"
|
||||
import { LiveAreaChart, type ChartSeries } from "./LiveAreaChart"
|
||||
|
||||
const MAX_POINTS = 40
|
||||
|
||||
type RateSample = { t: number; prompt: number; completion: number }
|
||||
|
||||
const SERIES: ChartSeries[] = [
|
||||
{ key: "prompt", label: "Prompt", color: "#f59e0b" },
|
||||
{ key: "completion", label: "Antwort", color: "#2dd4bf" },
|
||||
]
|
||||
|
||||
export function TokenPerformanceCard() {
|
||||
const { data: ts, dataUpdatedAt } = useTokenStats(3_000)
|
||||
const [hist, setHist] = useState<RateSample[]>([])
|
||||
const prev = useRef<{ p: number; c: number; t: number } | null>(null)
|
||||
|
||||
// Durchsatz (tok/s) aus den kumulativen Zählern ableiten: Delta / Zeitspanne pro Poll.
|
||||
useEffect(() => {
|
||||
if (!ts) return
|
||||
const now = Date.now()
|
||||
const p = ts.prompt_tokens, c = ts.completion_tokens
|
||||
if (prev.current) {
|
||||
const dt = Math.max((now - prev.current.t) / 1000, 0.001)
|
||||
const prompt = Math.max(0, (p - prev.current.p) / dt)
|
||||
const completion = Math.max(0, (c - prev.current.c) / dt)
|
||||
setHist((h) => [...h, { t: now, prompt, completion }].slice(-MAX_POINTS))
|
||||
}
|
||||
prev.current = { p, c, t: now }
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [dataUpdatedAt])
|
||||
|
||||
const last = hist[hist.length - 1]
|
||||
const curRate = last ? Math.round(last.prompt + last.completion) : 0
|
||||
|
||||
return (
|
||||
<div className="rounded-2xl border border-border/60 bg-gradient-to-b from-card/55 to-background/30 backdrop-blur-md p-5 shadow-lg shadow-black/20">
|
||||
<div className="mb-3 flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Activity className="h-4.5 w-4.5 text-primary" />
|
||||
<h2 className="text-sm font-semibold uppercase tracking-wide text-foreground">Token-Durchsatz</h2>
|
||||
<span className="ml-1 inline-flex items-center gap-1 text-[10px] font-medium text-muted-foreground/70">
|
||||
<span className="h-1.5 w-1.5 rounded-full bg-emerald-500 animate-pulse" /> live
|
||||
</span>
|
||||
</div>
|
||||
{ts && (
|
||||
<div className="mt-2 flex items-baseline gap-2">
|
||||
<span className="font-space text-3xl font-bold tracking-tight text-foreground tabular-nums">
|
||||
{curRate.toLocaleString("de-DE")}
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground">tok/s aktuell</span>
|
||||
</div>
|
||||
)}
|
||||
{ts && (
|
||||
<div className="mt-0.5 font-mono text-[11px] text-muted-foreground/70">
|
||||
{ts.total_tokens.toLocaleString("de-DE")} Tokens gesamt · {ts.saved_eur.toLocaleString("de-DE", { minimumFractionDigits: 2 })} € gespart
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex shrink-0 flex-col items-end gap-1.5 pt-1">
|
||||
{SERIES.map((s) => (
|
||||
<div key={s.key} className="flex items-center gap-1.5">
|
||||
<span className="h-2 w-2 rounded-full" style={{ background: s.color }} />
|
||||
<span className="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground">{s.label}</span>
|
||||
<span className="font-mono text-xs font-bold tabular-nums text-foreground">
|
||||
{Math.round((last?.[s.key as "prompt" | "completion"]) ?? 0)}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{ts ? (
|
||||
<LiveAreaChart data={hist} series={SERIES} unit=" tok/s" yMode="auto" height={150} />
|
||||
) : (
|
||||
<div className="flex h-[150px] items-center justify-center text-xs text-muted-foreground">Lade Durchsatz…</div>
|
||||
)}
|
||||
|
||||
<div className="mt-2 border-t border-border/30 pt-2 text-[10px] text-muted-foreground/70">
|
||||
Durchsatz aus dem Gateway abgeleitet (Prefill vs. Generierung). Flach bei Leerlauf.
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { useEffect, useState } from "react"
|
||||
import { useSystemStatus } from "@/lib/queries"
|
||||
|
||||
export type SysSample = { t: number; cpu: number; ram: number; gpu: number | null; disk: number | null }
|
||||
|
||||
const MAX_POINTS = 40 // bei 3s-Poll ~2 Min Live-Verlauf
|
||||
|
||||
/** Rollende Live-Historie der System-Metriken (CPU/RAM/GPU/Disk). Hängt bei jedem Poll
|
||||
* (dataUpdatedAt) einen Messpunkt an und hält die letzten 40. Geteilt von Zentrale +
|
||||
* Diagnose, damit beide denselben Verlauf aufbauen. */
|
||||
export function useSystemHistory() {
|
||||
const { data: sys, dataUpdatedAt, error } = useSystemStatus(3_000)
|
||||
const [hist, setHist] = useState<SysSample[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
if (!sys) return
|
||||
setHist((h) => [
|
||||
...h,
|
||||
{
|
||||
t: Date.now(),
|
||||
cpu: sys.cpu?.percent ?? 0,
|
||||
ram: sys.ram?.percent ?? 0,
|
||||
gpu: sys.gpu?.busy_percent ?? null,
|
||||
disk: sys.disk?.percent ?? null,
|
||||
},
|
||||
].slice(-MAX_POINTS))
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [dataUpdatedAt])
|
||||
|
||||
return { sys, hist, error }
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import { AgentStatusCard } from "@/components/dashboard/AgentStatusCard"
|
||||
import { RolesCard } from "@/components/dashboard/RolesCard"
|
||||
import { MemoryInputCard } from "@/components/dashboard/MemoryInputCard"
|
||||
import { TokenStatsCard } from "@/components/dashboard/TokenStatsCard"
|
||||
import { TokenPerformanceCard } from "@/components/dashboard/TokenPerformanceCard"
|
||||
|
||||
export function DashboardView() {
|
||||
return (
|
||||
@@ -26,6 +27,9 @@ export function DashboardView() {
|
||||
<UpdatesCard />
|
||||
</div>
|
||||
|
||||
{/* Live-Durchsatz (Performance) */}
|
||||
<TokenPerformanceCard />
|
||||
|
||||
{/* Bottom Grid: Agent, Roles, Memory & Token Stats */}
|
||||
<div className="grid gap-6 md:grid-cols-2 xl:grid-cols-4">
|
||||
<AgentStatusCard />
|
||||
|
||||
@@ -1,43 +1,35 @@
|
||||
import { useState } from "react"
|
||||
import { ExternalLink, RefreshCw, Save, Cpu, HardDrive, Cpu as GpuIcon, Activity } from "lucide-react"
|
||||
import { api } from "@/lib/api"
|
||||
import { useSystemStatus, useServices } from "@/lib/queries"
|
||||
import { useServices } from "@/lib/queries"
|
||||
import { useSystemHistory, type SysSample } from "@/lib/useSystemHistory"
|
||||
import { useDialog } from "@/lib/useDialog"
|
||||
import { cn, resolveExternalUrl } from "@/lib/utils"
|
||||
import { gb } from "@/lib/format"
|
||||
import { LiveAreaChart } from "@/components/dashboard/LiveAreaChart"
|
||||
|
||||
|
||||
function DiagnosticBar({ label, percent, detail, icon: Icon }: { label: string; percent: number; detail?: string; icon: any }) {
|
||||
const barColor = percent > 90
|
||||
? "bg-red-500 shadow-md shadow-red-500/20"
|
||||
: percent > 75
|
||||
? "bg-amber-500 shadow-md shadow-amber-500/20"
|
||||
: "bg-primary shadow-md shadow-primary/20"
|
||||
|
||||
function MetricChartCard({ label, percent, detail, icon: Icon, color, seriesKey, data }: {
|
||||
label: string; percent: number; detail?: string; icon: any
|
||||
color: string; seriesKey: keyof SysSample; data: SysSample[]
|
||||
}) {
|
||||
return (
|
||||
<div className="rounded-2xl border border-border/60 bg-card/45 backdrop-blur-md p-5 shadow-lg shadow-black/10 flex flex-col justify-between gap-3 hover:border-primary/30 transition-all duration-300">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="rounded-2xl border border-border/60 bg-card/45 backdrop-blur-md p-4 shadow-lg shadow-black/10 hover:border-primary/30 transition-all duration-300">
|
||||
<div className="mb-1 flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Icon className="h-4.5 w-4.5 text-primary" />
|
||||
<Icon className="h-4.5 w-4.5" style={{ color }} />
|
||||
<span className="text-xs font-semibold uppercase tracking-wider text-foreground">{label}</span>
|
||||
</div>
|
||||
<span className="text-xs font-mono font-bold text-foreground">{Math.round(percent)}%</span>
|
||||
<span className="font-mono text-sm font-bold tabular-nums text-foreground">{Math.round(percent)}%</span>
|
||||
</div>
|
||||
|
||||
<div className="w-full h-2 bg-background/40 rounded-full overflow-hidden border border-border/20">
|
||||
<div
|
||||
className={cn("h-full transition-all duration-700 ease-out", barColor)}
|
||||
style={{ width: `${Math.min(percent, 100)}%` }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{detail && <div className="text-[10px] font-mono text-muted-foreground/80">{detail}</div>}
|
||||
{detail && <div className="mb-1 font-mono text-[10px] text-muted-foreground/70">{detail}</div>}
|
||||
<LiveAreaChart data={data} series={[{ key: seriesKey as string, label, color }]} unit="%" yMode="percent" height={88} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function SystemView() {
|
||||
const { data: s, error: sErr } = useSystemStatus(3_000)
|
||||
const { sys: s, hist, error: sErr } = useSystemHistory()
|
||||
const { data: svc } = useServices(3_000)
|
||||
const { showAlert, dialogElement } = useDialog()
|
||||
const error = sErr ? String(sErr) : ""
|
||||
@@ -95,26 +87,29 @@ export function SystemView() {
|
||||
{s && (
|
||||
<div className="space-y-4">
|
||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
||||
<DiagnosticBar label="CPU" percent={s.cpu.percent} detail={s.cpu.cores ? `${s.cpu.cores} Cores` : undefined} icon={Cpu} />
|
||||
<DiagnosticBar label="RAM" percent={s.ram.percent} detail={`${gb(s.ram.used)} / ${gb(s.ram.total)} GB`} icon={Activity} />
|
||||
|
||||
<MetricChartCard label="CPU" color="#2dd4bf" seriesKey="cpu" data={hist}
|
||||
percent={s.cpu.percent} detail={s.cpu.cores ? `${s.cpu.cores} Cores` : undefined} icon={Cpu} />
|
||||
<MetricChartCard label="RAM" color="#38bdf8" seriesKey="ram" data={hist}
|
||||
percent={s.ram.percent} detail={`${gb(s.ram.used)} / ${gb(s.ram.total)} GB`} icon={Activity} />
|
||||
|
||||
{s.gpu && s.gpu.busy_percent != null && (
|
||||
<DiagnosticBar
|
||||
label="GPU"
|
||||
percent={s.gpu.busy_percent}
|
||||
<MetricChartCard
|
||||
label="GPU" color="#a78bfa" seriesKey="gpu" data={hist}
|
||||
percent={s.gpu.busy_percent}
|
||||
detail={
|
||||
s.gpu.gtt_used != null && s.gpu.gtt_total
|
||||
? `${gb(s.gpu.gtt_used)} / ${gb(s.gpu.gtt_total)} GB (GTT/unified)`
|
||||
: s.gpu.vram_used != null && s.gpu.vram_total
|
||||
? `${gb(s.gpu.vram_used)} / ${gb(s.gpu.vram_total)} GB VRAM`
|
||||
: undefined
|
||||
}
|
||||
icon={GpuIcon}
|
||||
}
|
||||
icon={GpuIcon}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
{s.disk && (
|
||||
<DiagnosticBar label="Disk" percent={s.disk.percent} detail={`${gb(s.disk.used)} / ${gb(s.disk.total)} GB`} icon={HardDrive} />
|
||||
<MetricChartCard label="Disk" color="#fbbf24" seriesKey="disk" data={hist}
|
||||
percent={s.disk.percent} detail={`${gb(s.disk.used)} / ${gb(s.disk.total)} GB`} icon={HardDrive} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user