Portability: sysfs-only sensors with thermal-zone fallback, /sys/block disk discovery, fs blacklist, pwrstat/NUT/apcupsd UPS, login shell from passwd, templated unit + install for any user, cross-compile targets

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Mitchell-Johnson
2026-09-17 13:43:44 -07:00
co-authored by Claude Fable 5.1
parent 3c6f4d4639
commit 7053cce7fe
9 changed files with 432 additions and 102 deletions
+20 -7
View File
@@ -1,5 +1,5 @@
import { ReactNode, useEffect, useRef, useState } from 'react'
import { api, Sample, Series } from './api'
import { api, Sample, Series, Temp } from './api'
import Sparkline from './Sparkline'
const MAX = 300
@@ -53,6 +53,19 @@ function Legend({ items }: { items: { name: string; color: string }[] }) {
return <div className="legend">{items.map((i) => <span key={i.name}><span className="sw" style={{ background: i.color }} />{i.name}</span>)}</div>
}
// Chips with many sensors (per-core temps on a big CPU) collapse to one row showing the hottest.
function collapseTemps(temps: Temp[]): Temp[] {
const byChip = new Map<string, Temp[]>()
for (const t of temps) (byChip.get(t.chip) ?? byChip.set(t.chip, []).get(t.chip)!).push(t)
const out: Temp[] = []
for (const [chip, list] of byChip) {
if (list.length <= 8) { out.push(...list); continue }
const hot = list.reduce((a, b) => (b.c > a.c ? b : a))
out.push({ ...hot, label: `${list.length} sensors · hottest ${hot.label}`, chip })
}
return out
}
export default function Dashboard() {
const [latest, setLatest] = useState<Sample | null>(null)
const [err, setErr] = useState('')
@@ -107,7 +120,7 @@ export default function Dashboard() {
<div className="dash">
{err && <div className="error">{err}</div>}
<div className="kpis">
<Tile label="CPU" value={pct(s.cpu)} sub={s.cores.length ? `cores ${s.cores.map((c) => Math.round(c)).join(' · ')}` : undefined}>
<Tile label="CPU" value={pct(s.cpu)} sub={s.cores.length === 0 ? undefined : s.cores.length <= 16 ? `cores ${s.cores.map((c) => Math.round(c)).join(' · ')}` : `${s.cores.length} cores · busiest ${pct(Math.max(...s.cores))}`}>
{h && <Sparkline series={[{ name: 'cpu', values: h.cpu, color: S1 }]} max={100} format={pct} height={36} />}
</Tile>
<Tile label="Memory" value={fmtBytes(s.mem.used)} sub={`of ${fmtBytes(s.mem.total)} · ${pct(memFrac * 100)}${s.swap.total ? ` · swap ${fmtBytes(s.swap.used)}` : ''}`}>
@@ -120,7 +133,7 @@ export default function Dashboard() {
<div className="cards">
<Card title="Temperatures">
{s.temps.length === 0 && <div className="empty">No temperature sensors reported by this host.</div>}
{s.temps.map((t) => {
{collapseTemps(s.temps).map((t) => {
const limit = t.crit || t.max || 100
const cls = t.c >= limit - 5 ? 'bad' : t.max && t.c >= t.max - 15 ? 'warn' : ''
return (
@@ -171,7 +184,7 @@ export default function Dashboard() {
<span>{d.name}</span>
<span className="model">{d.model}</span>
<span className="spacer" />
<span className="num">{d.temp ? `${d.temp.toFixed(0)}°C · ` : ''}{pct(d.util_pct)} busy</span>
<span className="num">{fmtBytes(d.size)} · {d.temp ? `${d.temp.toFixed(0)}°C · ` : ''}{pct(d.util_pct)} busy</span>
</div>
<div className="sub-head num">read {fmtRate(d.read_bps)}<span className="spacer" />write {fmtRate(d.write_bps)}</div>
{h?.disk[d.name] && (
@@ -193,14 +206,14 @@ export default function Dashboard() {
</Card>
<Card title="UPS">
{!s.ups && <div className="empty">No UPS data (pwrstat not available on this host).</div>}
{!s.ups && <div className="empty">No UPS daemon found (looks for pwrstat, NUT upsc, or apcaccess).</div>}
{s.ups && (
<>
<div className="sub-head">
<span className={'dot ' + (s.ups.source === 'Utility Power' ? 'ok' : 'bad')} />
<span className={'dot ' + (s.ups.on_battery ? 'bad' : 'ok')} />
<span>{s.ups.state} · {s.ups.source}</span>
<span className="spacer" />
<span className="model">{s.ups.model}</span>
<span className="model">{s.ups.model} · {s.ups.driver}</span>
</div>
<div className="row"><span className="k">Battery</span><Meter frac={s.ups.battery_pct / 100} cls={s.ups.battery_pct < 20 ? 'bad' : s.ups.battery_pct < 50 ? 'warn' : ''} /><span className="num">{pct(s.ups.battery_pct)} · {s.ups.runtime_min} min</span></div>
<div className="row"><span className="k">Load</span><Meter frac={s.ups.load_pct / 100} /><span className="num">{s.ups.load_w} W · {pct(s.ups.load_pct)}</span></div>