Dashboard tab: CPU/mem/load, hwmon temps+fans, net, disks, filesystems, UPS with 10-min sparklines

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Mitchell-Johnson
2026-09-17 13:37:39 -07:00
co-authored by Claude Fable 5.1
parent 24d64245ec
commit 3c6f4d4639
10 changed files with 982 additions and 34 deletions
+64
View File
@@ -0,0 +1,64 @@
import { useRef, useState } from 'react'
export interface SparkSeries { name: string; values: number[]; color: string }
interface Props {
series: SparkSeries[]
max?: number // fixed ceiling (e.g. 100 for percent); otherwise auto from data
format: (v: number) => string
height?: number
}
const W = 300
// Thin-line sparkline with a crosshair tooltip. Stroke stays 2px at any width via
// non-scaling-stroke; the box is stretched with preserveAspectRatio="none".
export default function Sparkline({ series, max, format, height = 44 }: Props) {
const box = useRef<HTMLDivElement>(null)
const [hover, setHover] = useState<number | null>(null)
const n = Math.max(...series.map((s) => s.values.length), 0)
if (n < 2) return <div className="spark" style={{ height }} />
let top = max ?? 0
if (max === undefined) for (const s of series) for (const v of s.values) if (v > top) top = v
if (top <= 0) top = 1
const H = height
const x = (i: number) => (i / (n - 1)) * W
const y = (v: number) => H - 2 - (Math.min(v, top) / top) * (H - 4)
const path = (vals: number[]) => {
const off = n - vals.length
return vals.map((v, i) => `${i === 0 ? 'M' : 'L'}${x(i + off).toFixed(1)},${y(v).toFixed(1)}`).join(' ')
}
const move = (clientX: number) => {
const r = box.current?.getBoundingClientRect()
if (!r || r.width === 0) return
const f = Math.min(1, Math.max(0, (clientX - r.left) / r.width))
setHover(Math.round(f * (n - 1)))
}
return (
<div ref={box} className="spark" style={{ height }}
onMouseMove={(e) => move(e.clientX)} onMouseLeave={() => setHover(null)}
onTouchStart={(e) => move(e.touches[0].clientX)} onTouchMove={(e) => move(e.touches[0].clientX)} onTouchEnd={() => setHover(null)}>
<svg viewBox={`0 0 ${W} ${H}`} preserveAspectRatio="none" aria-hidden="true">
<line x1={0} x2={W} y1={H - 2} y2={H - 2} stroke="var(--border)" strokeWidth={1} vectorEffect="non-scaling-stroke" />
{series.map((s) => (
<path key={s.name} d={path(s.values)} fill="none" stroke={s.color} strokeWidth={2}
strokeLinejoin="round" strokeLinecap="round" vectorEffect="non-scaling-stroke" />
))}
{hover !== null && (
<line x1={x(hover)} x2={x(hover)} y1={0} y2={H} stroke="var(--muted)" strokeWidth={1} vectorEffect="non-scaling-stroke" />
)}
</svg>
{hover !== null && (
<div className="tip" style={{ left: `${(hover / (n - 1)) * 100}%` }}>
{series.map((s) => {
const v = s.values[hover - (n - s.values.length)]
return v === undefined ? null : <span key={s.name}><span className="sw" style={{ background: s.color }} />{s.name} {format(v)} </span>
})}
</div>
)}
</div>
)
}