A Hardware tab: what the machine is made of
Per machine, like every other page. Four tiles — processor, memory, storage, graphics — over a card for each. Memory is the part that had nowhere to live before: every slot, with the module's size, type, form factor, rated and running speed, maker, part number, serial, rank and voltage, and the empty slots marked empty. A module whose maker is only a JEDEC id gets a name when it is one we know and keeps the number when it is not, because a wrong name is worse than a code. Everything is read from what Debian already has: dmidecode for the parts only the firmware knows, lscpu, lspci, sysfs. None of it changes under a running kernel, so it is read once and kept for ten minutes; Re-read is for the one thing that does change, a drive plugged in. Every reader is allowed to come back empty. A machine with no dmidecode, no root to read it with, or no PCI bus at all still gets a page, with a note saying what was missing and why. Checked against the Mac mini, which is all three cases at once: no SMBIOS memory table, a GPU only the device tree knows about, and cores that are not all alike — lscpu counts one cluster as "cores per socket" there, so without SMT the thread count is the honest answer. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
09dc846730
commit
776bcb236d
@@ -0,0 +1,211 @@
|
||||
// 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, 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>
|
||||
)
|
||||
}
|
||||
|
||||
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, 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
|
||||
|
||||
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) > 20 ? { fontSize: 16 } : undefined}>{cpu.model}</div>
|
||||
<div className="sub">{cores ? `${cores} core${cores === 1 ? '' : 's'}` : ''}{threads && threads !== cores ? ` · ${threads} threads` : ''}{cpu.max_mhz ? ` · up to ${ghz(cpu.max_mhz)}` : ''}</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} />
|
||||
<P k="Cores" v={cores ? `${cores}${cpu.threads_per_core && cpu.threads_per_core > 1 ? ` · ${cpu.threads_per_core} threads each` : ''}` : ''} />
|
||||
<P k="Clock" v={cpu.min_mhz && cpu.max_mhz ? `${ghz(cpu.min_mhz)} – ${ghz(cpu.max_mhz)}` : ghz(cpu.max_mhz)} />
|
||||
<P k="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>
|
||||
|
||||
<section className="card">
|
||||
<h2>System</h2>
|
||||
<div className="props">
|
||||
<P k="Model" v={sys.model} />
|
||||
<P k="Board" v={sys.board} />
|
||||
<P k="Chassis" v={sys.chassis} />
|
||||
<P k="Firmware" v={sys.bios} />
|
||||
<P k="Dated" v={sys.bios_date} />
|
||||
<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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user