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:
Mitchell-Johnson
2026-09-25 21:41:40 -07:00
co-authored by Claude Opus 5
parent 09dc846730
commit 776bcb236d
8 changed files with 1115 additions and 4 deletions
+4 -1
View File
@@ -33,6 +33,7 @@ import (
"hamlab/internal/config"
"hamlab/internal/cron"
"hamlab/internal/files"
"hamlab/internal/hardware"
"hamlab/internal/metrics"
"hamlab/internal/nodes"
"hamlab/internal/priv"
@@ -67,6 +68,7 @@ type Server struct {
tasks *tasks.Manager
updates *updates.Manager
storage *storage.Manager
hardware *hardware.Manager
space *space.Manager
update updateState
events eventLog
@@ -87,7 +89,7 @@ type Server struct {
func New(cfg *config.Config, sessions *auth.Sessions, mgr *term.Manager, col *metrics.Collector, st *store.Store, svc *services.Manager, cr *cron.Manager, tm *tasks.Manager, up *updates.Manager, sto *storage.Manager, fsLocal, fsSudo files.FS, nm *nodes.Manager, fingerprint string, ui fs.FS, version string) *Server {
s := &Server{cfg: cfg, sessions: sessions, mgr: mgr, metrics: col, sensors: sensors.NewRegistry(col), store: st, svc: svc, cron: cr, tasks: tm, updates: up, storage: sto, fsLocal: fsLocal, fsSudo: fsSudo, nodes: nm, fingerprint: fingerprint, ui: ui, version: version,
started: time.Now(), user: os.Getenv("USER")}
hardware: hardware.New(), started: time.Now(), user: os.Getenv("USER")}
self, err := os.Executable()
if err != nil {
self = "hamlab"
@@ -261,6 +263,7 @@ func New(cfg *config.Config, sessions *auth.Sessions, mgr *term.Manager, col *me
m.HandleFunc("POST /api/files/rename", s.auth(s.filesRename))
m.HandleFunc("POST /api/files/delete", s.auth(s.filesDelete))
m.HandleFunc("POST /api/files/chmod", s.auth(s.filesChmod))
m.HandleFunc("GET /api/hardware", s.auth(s.hardwareReport))
m.HandleFunc("GET /api/storage", s.auth(s.storageOverview))
m.HandleFunc("GET /api/storage/smart/{dev}", s.auth(s.storageSmart))
m.HandleFunc("POST /api/storage/smart/{dev}/test/{kind}", s.auth(s.storageSmartTest))