ForgeSight / frontend /src /components /TelemetryWidget.jsx
rasAli02's picture
πŸš€ Fix: Resolve TelemetryWidget crash (toFixed) and align backend response
d59a97d
import { useEffect, useRef, useState } from "react";
import { forgesight } from "@/lib/api";
import { Activity, Cpu, Zap, Thermometer, BarChart3, Database } from "lucide-react";
/* ── helpers ─────────────────────────────────────────────────────────────── */
function arc(pct, r = 38) {
const clamp = Math.min(100, Math.max(0, pct));
const angle = (clamp / 100) * 270 - 135; // 270Β° sweep, start -135Β°
const rad = (a) => (a * Math.PI) / 180;
const x = 50 + r * Math.cos(rad(angle));
const y = 50 + r * Math.sin(rad(angle));
const large = clamp > 50 ? 1 : 0;
const sx = 50 + r * Math.cos(rad(-135));
const sy = 50 + r * Math.sin(rad(-135));
return `M ${sx} ${sy} A ${r} ${r} 0 ${large} 1 ${x} ${y}`;
}
function ArcGauge({ pct = 0, label, value, icon: Icon, color = "#ED1C24" }) {
const prev = useRef(pct);
const [displayed, setDisplayed] = useState(pct);
useEffect(() => {
// smooth interpolation
const start = prev.current;
const end = pct;
const dur = 600;
const t0 = performance.now();
let raf;
const step = (now) => {
const progress = Math.min((now - t0) / dur, 1);
const eased = 1 - Math.pow(1 - progress, 3);
setDisplayed(start + (end - start) * eased);
if (progress < 1) raf = requestAnimationFrame(step);
else prev.current = end;
};
raf = requestAnimationFrame(step);
return () => cancelAnimationFrame(raf);
}, [pct]);
const bgPath = arc(100);
const fgPath = arc(displayed);
return (
<div className="flex flex-col items-center gap-1 min-w-0">
<div className="relative w-20 h-20 shrink-0">
<svg viewBox="0 0 100 100" className="w-full h-full -rotate-90" style={{ transform: "rotate(0deg)" }}>
{/* Track */}
<path d={bgPath} fill="none" stroke="#27272A" strokeWidth="8" strokeLinecap="round" />
{/* Value */}
<path
d={fgPath}
fill="none"
stroke={color}
strokeWidth="8"
strokeLinecap="round"
style={{ filter: `drop-shadow(0 0 4px ${color}66)` }}
/>
</svg>
{/* Center icon */}
<div className="absolute inset-0 flex items-center justify-center">
{Icon && <Icon className="w-4 h-4" style={{ color }} />}
</div>
</div>
<div className="font-mono text-sm text-white tabular-nums text-center leading-tight">
{value}
</div>
<div className="font-mono text-[10px] text-zinc-500 uppercase tracking-wider text-center">
{label}
</div>
</div>
);
}
function StatRow({ label, value, pct, color = "#ED1C24" }) {
return (
<div>
<div className="flex items-baseline justify-between mb-1">
<span className="font-mono text-[10px] text-zinc-500 uppercase tracking-wider">{label}</span>
<span className="font-mono text-xs text-white tabular-nums">{value}</span>
</div>
<div className="h-1 w-full bg-white/5 rounded-full overflow-hidden">
<div
className="h-full rounded-full transition-all duration-700 ease-out"
style={{
width: `${Math.max(2, Math.min(100, pct))}%`,
background: `linear-gradient(90deg, ${color}99, ${color})`,
boxShadow: `0 0 6px ${color}66`,
}}
/>
</div>
</div>
);
}
/* ── main component ──────────────────────────────────────────────────────── */
export default function TelemetryWidget() {
const [t, setT] = useState(null);
const [blink, setBlink] = useState(false);
useEffect(() => {
let alive = true;
const tick = async () => {
try {
const data = await forgesight.getTelemetry();
if (alive) {
setT(data);
setBlink((b) => !b);
}
} catch {}
};
tick();
const id = setInterval(tick, 2000);
return () => { alive = false; clearInterval(id); };
}, []);
const status = t?.status ?? "β€”";
const isLive = status === "Connected";
const isLimited = status === "Limited";
const statusColor = isLive ? "#10B981" : isLimited ? "#F59E0B" : "#71717A";
const statusLabel = isLive ? "LIVE" : isLimited ? "LIMITED" : "OFFLINE";
const vramPct = t ? (t.vram_used_gb / t.vram_total_gb) * 100 : 0;
const tokensPct = t ? (t.tokens_per_sec / 4000) * 100 : 0;
const powerPct = t ? (t.power_watts / 750) * 100 : 0;
return (
<div
className="border bg-[#0d0d10] p-5 fs-corners"
style={{
borderColor: isLive ? "#10B98133" : "#27272A",
boxShadow: isLive ? "0 0 20px #10B98111" : "none",
transition: "border-color 0.6s, box-shadow 0.6s",
}}
data-testid="telemetry-widget"
>
{/* Header */}
<div className="flex items-center justify-between mb-5">
<div className="flex items-center gap-2">
<Activity className="w-3.5 h-3.5" style={{ color: statusColor }} />
<span className="fs-label">Live Telemetry</span>
</div>
<div className="flex items-center gap-2">
{/* Pulse dot */}
<span
className="w-2 h-2 rounded-full inline-block"
style={{
background: statusColor,
boxShadow: isLive ? `0 0 6px ${statusColor}` : "none",
opacity: isLive && blink ? 1 : isLive ? 0.5 : 0.3,
transition: "opacity 0.5s",
}}
/>
<span
className="font-mono text-[10px] tracking-widest px-2 py-0.5 border rounded"
style={{ color: statusColor, borderColor: `${statusColor}44`, background: `${statusColor}11` }}
data-testid="telemetry-status-badge"
>
{statusLabel}
</span>
</div>
</div>
{/* Device */}
<div className="font-mono text-[10px] text-zinc-600 mb-4 flex items-center justify-between">
<span>{t?.device ?? "AMD Instinct MI300X"}</span>
{t?.persistence && (
<span className="flex items-center gap-1 text-zinc-600">
<Database className="w-2.5 h-2.5" />
{t.persistence}
</span>
)}
</div>
{/* Arc gauges row */}
<div className="grid grid-cols-3 gap-2 mb-5 pb-5 border-b border-white/5">
<ArcGauge
pct={t?.gpu_util_pct ?? 0}
label="GPU Util"
value={t?.gpu_util_pct != null ? `${t.gpu_util_pct.toFixed(0)}%` : "β€”"}
icon={Cpu}
color={isLive ? "#ED1C24" : "#3F3F46"}
/>
<ArcGauge
pct={vramPct}
label="VRAM"
value={t?.vram_used_gb != null ? `${t.vram_used_gb.toFixed(0)}G` : "β€”"}
icon={BarChart3}
color={isLive ? "#F59E0B" : "#3F3F46"}
/>
<ArcGauge
pct={t?.temp_c ? (t.temp_c / 90) * 100 : 0}
label="Temp"
value={t?.temp_c != null ? `${t.temp_c.toFixed(0)}Β°C` : "β€”"}
icon={Thermometer}
color={isLive ? "#06B6D4" : "#3F3F46"}
/>
</div>
{/* Bar stats */}
<div className="space-y-3">
<StatRow
label="Tokens/sec"
value={t ? t.tokens_per_sec.toLocaleString() : "β€”"}
pct={tokensPct}
color={isLive ? "#10B981" : "#3F3F46"}
/>
<StatRow
label="Power Draw"
value={t ? `${t.power_watts} W` : "β€”"}
pct={powerPct}
color={isLive ? "#ED1C24" : "#3F3F46"}
/>
<StatRow
label="VRAM Used"
value={t?.vram_used_gb != null ? `${t.vram_used_gb.toFixed(1)} / ${t.vram_total_gb} GB` : "β€”"}
pct={vramPct}
color={isLive ? "#F59E0B" : "#3F3F46"}
/>
</div>
</div>
);
}