Found by a headless pass over every page on every machine at desktop and phone widths, then every interaction on the hub. BUGS.md has the list, what was checked and found fine, and the quality-of-life notes to discuss. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
263 lines
12 KiB
TypeScript
263 lines
12 KiB
TypeScript
// 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
|
||
|
||
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)}` : ''].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} />
|
||
<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>
|
||
|
||
<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>
|
||
)
|
||
}
|