Files
hamlab/web/src/Hardware.tsx
T
Mitchell-JohnsonandClaude Opus 5.5 1e2dcacb2c Two-socket machines: Hardware and Top show each CPU, not one with every core
Hardware counts CPUs, cores and threads per CPU. Top groups threads under a row
per socket, reads each core's temperature from its own socket's coretemp chip,
and the CPU temperature is the hotter package.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-10 00:04:47 -08:00

272 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Hardware: what this machine is made of — the board, the processor, the memory modules in
// their slots, the drives and the graphics. None of it moves, so the page is a read rather
// than a monitor: what is live (temperature, usage, SMART) belongs to Dashboard and Disks.
import { useCallback, useEffect, useState } from 'react'
import { api, Hardware as HW, HWBoard, HWDrive, HWGPU, HWModule } from './api'
import { fmtBytes } from './fmt'
import { Btn, Loading } from './ui'
const KIND: Record<string, string> = { nvme: 'NVMe', ssd: 'SSD', hdd: 'HDD', usb: 'USB', emmc: 'eMMC', sd: 'SD', other: 'disk' }
const ghz = (mhz?: number) => (mhz ? `${(mhz / 1000).toFixed(mhz >= 1000 ? 2 : 1)} GHz` : '')
// A row of the "key: value" kind, dropped entirely when there is no value — a machine that
// doesn't report its serial should show one fewer line, not an empty one.
function P({ k, v, mono }: { k: string; v?: string | number | null | false; mono?: boolean }) {
if (v === undefined || v === null || v === false || v === '' || v === 0) return null
return <div><span className="k">{k}</span><span className={mono ? 'mono' : undefined} title={String(v)}>{v}</span></div>
}
function Module({ m }: { m: HWModule }) {
if (m.empty) {
return (
<div className="sub-card">
<div className="sub-head"><span className="dname muted">{m.locator}</span><span className="spacer" /><span className="badge">empty</span></div>
</div>
)
}
const speed = m.running_mts && m.speed_mts && m.running_mts !== m.speed_mts
? `${m.running_mts} MT/s (rated ${m.speed_mts})` // the board is running it slower than it could
: m.running_mts || m.speed_mts ? `${m.running_mts || m.speed_mts} MT/s` : ''
return (
<div className="sub-card">
<div className="sub-head">
<span className="dname">{fmtBytes(m.size ?? 0)} {m.type}</span>
<span className="spacer" />
<span className="model mono">{m.locator}</span>
</div>
<div className="props">
<P k="Maker" v={m.maker} />
<P k="Form" v={m.form_factor} />
<P k="Part" v={m.part} mono />
<P k="Speed" v={speed} />
<P k="Rank" v={m.rank ? `${m.rank}R` : ''} />
<P k="Voltage" v={m.voltage} />
<P k="Serial" v={m.serial} mono />
</div>
</div>
)
}
function Drive({ d }: { d: HWDrive }) {
return (
<div className="sub-card">
<div className="sub-head">
<span className="dname">{d.model || d.name}</span>
<span className="spacer" />
<span className="badge">{KIND[d.kind] ?? d.kind}</span>
</div>
<div className="props">
<P k="Size" v={fmtBytes(d.size)} />
<P k="Device" v={`/dev/${d.name}`} mono />
<P k="Bus" v={d.tran ? d.tran.toUpperCase() : ''} />
<P k="Spins" v={d.rotational ? 'yes — mechanical' : 'no — solid state'} />
<P k="Firmware" v={d.firmware} mono />
<P k="Serial" v={d.serial} mono />
<P k="Vendor" v={d.vendor} />
</div>
</div>
)
}
function Gpu({ g }: { g: HWGPU }) {
return (
<div className="sub-card">
<div className="sub-head">
<span className="dname">{g.model}</span>
<span className="spacer" />
{g.class && <span className="badge">{g.class}</span>}
</div>
<div className="props">
<P k="Vendor" v={g.vendor} />
<P k="Driver" v={g.driver} mono />
<P k="Memory" v={g.memory ? fmtBytes(g.memory) : ''} />
<P k="Slot" v={g.slot} mono />
<P k="Node" v={g.node} mono />
</div>
</div>
)
}
// The board and the firmware on it. Everything here is SMBIOS, so a machine without one
// — a Mac, a Pi — gets a card that says as much instead of a column of blanks.
function Motherboard({ b }: { b: HWBoard }) {
const named = b.maker || b.product || b.bios
// A board that reports every slot "Available" is not telling us anything — mini PCs
// copy the slot table out of a reference design and list slots the case has no room
// for. Only worth showing when the firmware actually tracks what is plugged in.
const slots = (b.slots ?? []).some((s) => s.usage === 'In Use') ? (b.slots ?? []) : []
return (
<section className="card">
<h2>Motherboard</h2>
{!named && <div className="empty">This machine has no SMBIOS table, so its firmware doesn't say what board it is.</div>}
{named && (
<div className="props">
<P k="Maker" v={b.maker} />
<P k="Model" v={b.product} />
<P k="Version" v={b.version} />
<P k="Kind" v={b.type} />
<P k="Chassis" v={[b.chassis_maker, b.chassis].filter(Boolean).join(' ')} />
<P k="In chassis" v={b.location} />
<P k="Serial" v={b.serial} mono />
<P k="Asset tag" v={b.asset_tag} mono />
</div>
)}
{(b.bios || b.bios_date) && (
<div className="props" style={{ marginTop: 8 }}>
<P k="Firmware" v={b.bios} />
<P k="Revision" v={b.bios_revision} />
<P k="Dated" v={b.bios_date} />
<P k="ROM" v={b.rom_size} />
<P k="Boots" v={b.uefi ? 'UEFI' : ''} />
</div>
)}
{slots.length > 0 && (
<div style={{ marginTop: 8 }}>
<div className="sub-head"><span className="dname">Expansion slots</span><span className="spacer" /><span className="muted small">as the firmware lists them</span></div>
<table className="dtable">
<thead><tr><th>Slot</th><th>Type</th><th>What is in it</th></tr></thead>
<tbody>{slots.map((s, i) => (
<tr key={s.designation + i}>
<td className="mono small">{s.designation}</td>
<td className="mono small">{[s.type, s.length && s.length !== 'Other' ? s.length.toLowerCase() : ''].filter(Boolean).join(' · ')}</td>
<td className="small" title={s.bus}>{s.usage === 'In Use' ? (s.device ?? s.bus ?? 'in use') : <span className="muted">{(s.usage ?? '').toLowerCase() || '—'}</span>}</td>
</tr>
))}</tbody>
</table>
</div>
)}
</section>
)
}
export default function Hardware() {
const [hw, setHw] = useState<HW | null>(null)
const [err, setErr] = useState('')
const [busy, setBusy] = useState(false)
const load = useCallback(async (refresh = false) => {
if (refresh) setBusy(true)
try { setHw(await api.hardware(refresh)); setErr('') } catch (e) { setErr(e instanceof Error ? e.message : 'failed') } finally { setBusy(false) }
}, [])
useEffect(() => { load() }, [load])
if (err) return <div className="dash"><div className="notice">{err === 'not found' ? "This machine's Ham Lab is older than the Hardware tab. Update it from Machines." : err}</div></div>
if (!hw) return <div className="dash"><Loading label="Reading the machine…" /></div>
const { system: sys, board, cpu, memory: mem } = hw
const drives = hw.drives ?? []
const gpus = hw.gpus ?? []
const capacity = drives.reduce((n, d) => n + d.size, 0)
const threads = cpu.threads ?? 0
const cores = cpu.cores ?? 0
// a two-socket board is two processors, not one with twice the cores: count and name each
const sockets = Math.max(1, cpu.sockets ?? 1)
const multi = sockets > 1
const per = (n: number) => Math.round(n / sockets)
return (
<div className="dash">
<div className="dash-bar">
<span className="status"><b>{sys.model || 'this machine'}</b></span>
{sys.virt && <span className="badge" title="this is a virtual machine, so the parts below are what the host handed it">{sys.virt}</span>}
<span className="spacer" style={{ flex: 1 }} />
<Btn className="btn" busy={busy} onClick={() => load(true)}>Re-read</Btn>
</div>
<div className="kpis">
<div className="tile">
<div className="label">Processor</div>
<div className="value" title={cpu.model} style={(cpu.model?.length ?? 0) > (multi ? 16 : 20) ? { fontSize: 16 } : undefined}>{multi ? `${sockets} × ${cpu.model}` : cpu.model}</div>
<div className="sub">{(multi
? [`${sockets} CPUs`, cores ? `${per(cores)} cores${threads && threads !== cores ? ` / ${per(threads)} threads` : ''} each` : '', cpu.max_mhz ? `up to ${ghz(cpu.max_mhz)}` : '']
: [cores ? `${cores} core${cores === 1 ? '' : 's'}` : '', threads && threads !== cores ? `${threads} threads` : '', cpu.max_mhz ? `up to ${ghz(cpu.max_mhz)}` : '']
).filter(Boolean).join(' · ')}</div>
</div>
<div className="tile">
<div className="label">Memory</div>
<div className="value">{fmtBytes(mem.installed || mem.total)}</div>
<div className="sub">{mem.slots ? `${mem.filled ?? 0} of ${mem.slots} slot${mem.slots === 1 ? '' : 's'} filled` : 'slots unknown'}{mem.ecc ? ` · ${mem.ecc}` : ''}</div>
</div>
<div className="tile">
<div className="label">Storage</div>
<div className="value">{fmtBytes(capacity)}</div>
<div className="sub">{drives.length} drive{drives.length === 1 ? '' : 's'}</div>
</div>
<div className="tile">
<div className="label">Graphics</div>
<div className="value" title={gpus[0]?.model} style={(gpus[0]?.model?.length ?? 0) > 20 ? { fontSize: 16 } : undefined}>{gpus[0]?.model ?? 'none found'}</div>
<div className="sub">{gpus.length > 1 ? `${gpus.length} adapters` : gpus[0]?.driver ? `${gpus[0].driver} driver` : ''}</div>
</div>
</div>
{(hw.notes ?? []).map((n, i) => <div className="notice" key={i}>{n}</div>)}
<div className="cards">
<section className="card">
<h2>Processor</h2>
<div className="props">
<P k="Model" v={cpu.model} />
<P k="Vendor" v={cpu.vendor} />
{multi && <P k="CPUs" v={`${sockets}`} />}
<P k="Cores" v={cores ? `${multi ? `${per(cores)} per CPU · ${cores} in all` : cores}${cpu.threads_per_core && cpu.threads_per_core > 1 ? ` · ${cpu.threads_per_core} threads each` : ''}` : ''} />
{multi && threads > 0 && <P k="Threads" v={`${per(threads)} per CPU · ${threads} in all`} />}
<P k="Clock" v={cpu.min_mhz && cpu.max_mhz ? `${ghz(cpu.min_mhz)} – ${ghz(cpu.max_mhz)}` : ghz(cpu.max_mhz)} />
<P k={multi ? 'Sockets' : 'Socket'} v={(cpu.socket_names ?? []).join(', ')} />
<P k="Arch" v={cpu.arch} />
<P k="Family" v={[cpu.family && `family ${cpu.family}`, cpu.model_id && `model ${cpu.model_id}`, cpu.stepping && `stepping ${cpu.stepping}`].filter(Boolean).join(' · ')} />
<P k="Microcode" v={cpu.microcode} mono />
<P k="Virt" v={cpu.virtualization} />
</div>
{(cpu.caches ?? []).length > 0 && (
<div className="props" style={{ marginTop: 8 }}>
{(cpu.caches ?? []).map((c) => <P key={c.level} k={c.level} v={c.size} />)}
</div>
)}
{(cpu.flags ?? []).length > 0 && (
<div className="chips" style={{ marginTop: 8 }}>{(cpu.flags ?? []).map((f) => <span className="badge" key={f}>{f}</span>)}</div>
)}
</section>
<section className="card">
<h2>Memory</h2>
<div className="props">
<P k="Installed" v={mem.installed ? fmtBytes(mem.installed) : ''} />
<P k="Usable" v={fmtBytes(mem.total)} />
<P k="Board takes" v={mem.max_capacity ? fmtBytes(mem.max_capacity) : ''} />
<P k="ECC" v={mem.ecc} />
</div>
<div style={{ marginTop: 8 }}>
{(mem.modules ?? []).map((m, i) => <Module key={m.locator + i} m={m} />)}
{(mem.modules ?? []).length === 0 && <div className="empty">The firmware doesn't say what is in the slots.</div>}
</div>
</section>
<section className="card">
<h2>Drives</h2>
{drives.map((d) => <Drive key={d.name} d={d} />)}
{drives.length === 0 && <div className="empty">No drives found.</div>}
</section>
<section className="card">
<h2>Graphics</h2>
{gpus.map((g, i) => <Gpu key={(g.slot ?? '') + i} g={g} />)}
{gpus.length === 0 && <div className="empty">No display adapter found.</div>}
</section>
<Motherboard b={board} />
<section className="card">
<h2>System</h2>
<div className="props">
<P k="Model" v={sys.model} />
<P k="Family" v={sys.family} />
<P k="Serial" v={sys.serial} mono />
<P k="Kernel" v={sys.kernel} mono />
<P k="Arch" v={sys.arch} />
<P k="Virtual" v={sys.virt} />
</div>
</section>
</div>
</div>
)
}