Hardware: a Motherboard card
The board, the chassis and the firmware on it, which the System card was reducing to two lines. Maker, model, version, serial and asset tag; chassis and where the board sits in it; the firmware's vendor, version, revision, date, ROM size and whether the machine boots UEFI — that last one is a line in SMBIOS's characteristics list rather than a field, so the parser now keeps those lists too. Expansion slots come with it, with the device found at each occupied slot's PCI address rather than the address alone. They are only shown when the firmware tracks occupancy at all: core's NucBox lists five PCIe x1 slots it does not physically have, all "Available", copied out of a reference design. grid's board reports real addresses and real occupancy, which is the case worth printing. A machine with no SMBIOS gets a card that says so. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
38039d1adc
commit
03e0761f3e
+124
-23
@@ -26,15 +26,44 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type System struct {
|
type System struct {
|
||||||
Model string `json:"model,omitempty"` // "GMKtec NucBox G3"
|
Model string `json:"model,omitempty"` // "GMKtec NucBox G3"
|
||||||
Serial string `json:"serial,omitempty"`
|
Serial string `json:"serial,omitempty"`
|
||||||
Board string `json:"board,omitempty"` // motherboard, when it differs from the model
|
Family string `json:"family,omitempty"`
|
||||||
Chassis string `json:"chassis,omitempty"`
|
Kernel string `json:"kernel,omitempty"`
|
||||||
BIOS string `json:"bios,omitempty"`
|
Arch string `json:"arch,omitempty"`
|
||||||
BIOSDate string `json:"bios_date,omitempty"`
|
Virt string `json:"virt,omitempty"` // "kvm", "lxc", … when this is not real hardware
|
||||||
Kernel string `json:"kernel,omitempty"`
|
}
|
||||||
Arch string `json:"arch,omitempty"`
|
|
||||||
Virt string `json:"virt,omitempty"` // "kvm", "lxc", … when this is not real hardware
|
// Slot is an expansion slot as the firmware describes it. Worth treating with suspicion on
|
||||||
|
// a mini PC, where the table is often copied from a reference design and lists slots the
|
||||||
|
// case has no room for.
|
||||||
|
type Slot struct {
|
||||||
|
Designation string `json:"designation"`
|
||||||
|
Type string `json:"type,omitempty"`
|
||||||
|
Usage string `json:"usage,omitempty"` // Available | In Use | Unknown
|
||||||
|
Length string `json:"length,omitempty"`
|
||||||
|
Width string `json:"width,omitempty"`
|
||||||
|
Bus string `json:"bus,omitempty"` // PCI address of what is in it
|
||||||
|
Device string `json:"device,omitempty"` // and what that turns out to be
|
||||||
|
}
|
||||||
|
|
||||||
|
// Board is the motherboard and the firmware on it.
|
||||||
|
type Board struct {
|
||||||
|
Maker string `json:"maker,omitempty"`
|
||||||
|
Product string `json:"product,omitempty"`
|
||||||
|
Version string `json:"version,omitempty"`
|
||||||
|
Serial string `json:"serial,omitempty"`
|
||||||
|
AssetTag string `json:"asset_tag,omitempty"`
|
||||||
|
Type string `json:"type,omitempty"` // "Motherboard", "Processor Module", …
|
||||||
|
Location string `json:"location,omitempty"` // where it sits in the chassis
|
||||||
|
Chassis string `json:"chassis,omitempty"`
|
||||||
|
ChassisMaker string `json:"chassis_maker,omitempty"`
|
||||||
|
BIOS string `json:"bios,omitempty"` // vendor and version
|
||||||
|
BIOSDate string `json:"bios_date,omitempty"`
|
||||||
|
BIOSRevision string `json:"bios_revision,omitempty"`
|
||||||
|
ROMSize string `json:"rom_size,omitempty"`
|
||||||
|
UEFI bool `json:"uefi,omitempty"`
|
||||||
|
Slots []Slot `json:"slots,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Cache struct {
|
type Cache struct {
|
||||||
@@ -114,6 +143,7 @@ type GPU struct {
|
|||||||
type Report struct {
|
type Report struct {
|
||||||
At time.Time `json:"at"`
|
At time.Time `json:"at"`
|
||||||
System System `json:"system"`
|
System System `json:"system"`
|
||||||
|
Board Board `json:"board"`
|
||||||
CPU CPU `json:"cpu"`
|
CPU CPU `json:"cpu"`
|
||||||
Memory Memory `json:"memory"`
|
Memory Memory `json:"memory"`
|
||||||
Drives []Drive `json:"drives"`
|
Drives []Drive `json:"drives"`
|
||||||
@@ -159,6 +189,7 @@ func collect(ctx context.Context) *Report {
|
|||||||
r.Notes = append(r.Notes, note)
|
r.Notes = append(r.Notes, note)
|
||||||
}
|
}
|
||||||
r.System = system(ctx, dmi)
|
r.System = system(ctx, dmi)
|
||||||
|
r.Board = board(dmi, pciNames(ctx))
|
||||||
r.CPU = cpu(ctx, dmi)
|
r.CPU = cpu(ctx, dmi)
|
||||||
r.Memory = memory(dmi)
|
r.Memory = memory(dmi)
|
||||||
if len(r.Memory.Modules) == 0 && note == "" {
|
if len(r.Memory.Modules) == 0 && note == "" {
|
||||||
@@ -180,6 +211,10 @@ type record struct {
|
|||||||
V map[string]string
|
V map[string]string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// listKey holds every doubly-indented line of a record — dmidecode's lists, which have no
|
||||||
|
// key of their own: BIOS characteristics, board features, slot characteristics.
|
||||||
|
const listKey = "\x00list"
|
||||||
|
|
||||||
var handleRe = regexp.MustCompile(`^Handle 0x[0-9A-Fa-f]+, DMI type (\d+)`)
|
var handleRe = regexp.MustCompile(`^Handle 0x[0-9A-Fa-f]+, DMI type (\d+)`)
|
||||||
|
|
||||||
// readDMI runs dmidecode once for every type the page wants and parses the lot. It needs
|
// readDMI runs dmidecode once for every type the page wants and parses the lot. It needs
|
||||||
@@ -193,7 +228,7 @@ func readDMI(ctx context.Context) ([]record, string) {
|
|||||||
}
|
}
|
||||||
tool = "/usr/sbin/dmidecode"
|
tool = "/usr/sbin/dmidecode"
|
||||||
}
|
}
|
||||||
args := []string{tool, "-t", "bios", "-t", "system", "-t", "baseboard", "-t", "chassis", "-t", "processor", "-t", "memory"}
|
args := []string{tool, "-t", "bios", "-t", "system", "-t", "baseboard", "-t", "chassis", "-t", "slot", "-t", "processor", "-t", "memory"}
|
||||||
if os.Geteuid() != 0 {
|
if os.Geteuid() != 0 {
|
||||||
if !priv.Available() {
|
if !priv.Available() {
|
||||||
return nil, "the board and the memory modules come from the firmware's table, which only root may read"
|
return nil, "the board and the memory modules come from the firmware's table, which only root may read"
|
||||||
@@ -223,12 +258,16 @@ func parse(text string) []record {
|
|||||||
cur = &recs[len(recs)-1]
|
cur = &recs[len(recs)-1]
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if cur == nil || !strings.HasPrefix(line, "\t") || strings.HasPrefix(line, "\t\t") {
|
if cur == nil || !strings.HasPrefix(line, "\t") {
|
||||||
if strings.TrimSpace(line) == "" {
|
if strings.TrimSpace(line) == "" {
|
||||||
cur = nil
|
cur = nil
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if strings.HasPrefix(line, "\t\t") { // a list under the field above it, e.g. BIOS characteristics
|
||||||
|
cur.V[listKey] += strings.TrimSpace(line) + "\n"
|
||||||
|
continue
|
||||||
|
}
|
||||||
k, v, ok := strings.Cut(strings.TrimSpace(line), ":")
|
k, v, ok := strings.Cut(strings.TrimSpace(line), ":")
|
||||||
if !ok {
|
if !ok {
|
||||||
continue
|
continue
|
||||||
@@ -280,19 +319,12 @@ func all(recs []record, t int) []record {
|
|||||||
// ---- system ----
|
// ---- system ----
|
||||||
|
|
||||||
func system(ctx context.Context, dmi []record) System {
|
func system(ctx context.Context, dmi []record) System {
|
||||||
sys, board, bios, chassis := first(dmi, 1), first(dmi, 2), first(dmi, 0), first(dmi, 3)
|
sys := first(dmi, 1)
|
||||||
s := System{
|
s := System{
|
||||||
Model: join(sys.get("Manufacturer"), sys.get("Product Name")),
|
Model: join(sys.get("Manufacturer"), sys.get("Product Name")),
|
||||||
Serial: sys.get("Serial Number"),
|
Serial: sys.get("Serial Number"),
|
||||||
Board: join(board.get("Manufacturer"), board.get("Product Name")),
|
Family: sys.get("Family"),
|
||||||
Chassis: chassis.get("Type"),
|
Arch: runtimeArch(),
|
||||||
BIOS: join(bios.get("Vendor"), bios.get("Version")),
|
|
||||||
BIOSDate: bios.get("Release Date"),
|
|
||||||
Arch: runtimeArch(),
|
|
||||||
}
|
|
||||||
// a mini PC names its board after itself — "GMKtec" under "GMKtec NucBox G3" says nothing
|
|
||||||
if s.Board != "" && (s.Board == s.Model || strings.Contains(strings.ToLower(s.Model), strings.ToLower(s.Board))) {
|
|
||||||
s.Board = ""
|
|
||||||
}
|
}
|
||||||
s.Kernel = readFile("/proc/sys/kernel/osrelease")
|
s.Kernel = readFile("/proc/sys/kernel/osrelease")
|
||||||
if s.Model == "" { // no SMBIOS: a Pi, a Mac, anything with a device tree
|
if s.Model == "" { // no SMBIOS: a Pi, a Mac, anything with a device tree
|
||||||
@@ -306,6 +338,53 @@ func system(ctx context.Context, dmi []record) System {
|
|||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// board reads the motherboard, the chassis it sits in, the firmware on it and the
|
||||||
|
// expansion slots the firmware claims. All of it is SMBIOS, so a machine without one — a
|
||||||
|
// Mac, a Pi — gets an empty board, and the page says so rather than inventing one.
|
||||||
|
func board(dmi []record, pci map[string]string) Board {
|
||||||
|
b, ch, bios := first(dmi, 2), first(dmi, 3), first(dmi, 0)
|
||||||
|
out := Board{
|
||||||
|
Maker: b.get("Manufacturer"),
|
||||||
|
Product: b.get("Product Name"),
|
||||||
|
Version: b.get("Version"),
|
||||||
|
Serial: b.get("Serial Number"),
|
||||||
|
AssetTag: b.get("Asset Tag"),
|
||||||
|
Type: b.get("Type"),
|
||||||
|
Location: b.get("Location In Chassis"),
|
||||||
|
Chassis: ch.get("Type"),
|
||||||
|
ChassisMaker: ch.get("Manufacturer"),
|
||||||
|
BIOS: join(bios.get("Vendor"), bios.get("Version")),
|
||||||
|
BIOSDate: bios.get("Release Date"),
|
||||||
|
BIOSRevision: bios.get("BIOS Revision"),
|
||||||
|
}
|
||||||
|
// "ROM Size: 0 MB" is a board that never filled the field in, not a board with no ROM
|
||||||
|
if rom := bios.get("ROM Size"); rom != "" && !strings.HasPrefix(rom, "0 ") {
|
||||||
|
out.ROMSize = rom
|
||||||
|
}
|
||||||
|
if out.Product == out.Maker {
|
||||||
|
out.Product = "" // a board named after its maker says nothing twice
|
||||||
|
}
|
||||||
|
out.UEFI = strings.Contains(bios.V[listKey], "UEFI") // a characteristics line, not a field
|
||||||
|
for _, sl := range all(dmi, 9) {
|
||||||
|
s := Slot{
|
||||||
|
Designation: sl.get("Designation"),
|
||||||
|
Type: sl.get("Type"),
|
||||||
|
Usage: sl.get("Current Usage"),
|
||||||
|
Length: sl.get("Length"),
|
||||||
|
Width: sl.get("Data Bus Width"),
|
||||||
|
}
|
||||||
|
// every slot on some boards claims 0000:00:00.0, which is the host bridge and
|
||||||
|
// certainly not what is plugged into that slot
|
||||||
|
if bus := sl.get("Bus Address"); bus != "" && bus != "0000:00:00.0" {
|
||||||
|
s.Bus, s.Device = bus, pci[bus]
|
||||||
|
}
|
||||||
|
if s.Designation != "" {
|
||||||
|
out.Slots = append(out.Slots, s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
func join(parts ...string) string {
|
func join(parts ...string) string {
|
||||||
var keep []string
|
var keep []string
|
||||||
for _, p := range parts {
|
for _, p := range parts {
|
||||||
@@ -667,6 +746,28 @@ func gpus(ctx context.Context) []GPU {
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// pciNames maps a PCI address to a readable name, so a slot the firmware says is in use
|
||||||
|
// can say what is in it.
|
||||||
|
func pciNames(ctx context.Context) map[string]string {
|
||||||
|
m := map[string]string{}
|
||||||
|
if _, err := exec.LookPath("lspci"); err != nil {
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
out, err := exec.CommandContext(ctx, "lspci", "-D", "-mm", "-nn").Output()
|
||||||
|
if err != nil {
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
for _, line := range strings.Split(string(out), "\n") {
|
||||||
|
slot, _, _ := strings.Cut(line, " ")
|
||||||
|
f := quoted.FindAllStringSubmatch(line, -1)
|
||||||
|
if slot == "" || len(f) < 3 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
m[slot] = strings.TrimSpace(stripID(f[1][1]) + " " + stripID(f[2][1]))
|
||||||
|
}
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
func displayClass(c string) string {
|
func displayClass(c string) string {
|
||||||
switch {
|
switch {
|
||||||
case strings.Contains(c, "[0300]"):
|
case strings.Contains(c, "[0300]"):
|
||||||
|
|||||||
+57
-6
@@ -2,7 +2,7 @@
|
|||||||
// their slots, the drives and the graphics. None of it moves, so the page is a read rather
|
// 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.
|
// than a monitor: what is live (temperature, usage, SMART) belongs to Dashboard and Disks.
|
||||||
import { useCallback, useEffect, useState } from 'react'
|
import { useCallback, useEffect, useState } from 'react'
|
||||||
import { api, Hardware as HW, HWDrive, HWGPU, HWModule } from './api'
|
import { api, Hardware as HW, HWBoard, HWDrive, HWGPU, HWModule } from './api'
|
||||||
import { fmtBytes } from './fmt'
|
import { fmtBytes } from './fmt'
|
||||||
import { Btn, Loading } from './ui'
|
import { Btn, Loading } from './ui'
|
||||||
|
|
||||||
@@ -87,6 +87,58 @@ function Gpu({ g }: { g: HWGPU }) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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() {
|
export default function Hardware() {
|
||||||
const [hw, setHw] = useState<HW | null>(null)
|
const [hw, setHw] = useState<HW | null>(null)
|
||||||
const [err, setErr] = useState('')
|
const [err, setErr] = useState('')
|
||||||
@@ -100,7 +152,7 @@ export default function Hardware() {
|
|||||||
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 (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>
|
if (!hw) return <div className="dash"><Loading label="Reading the machine…" /></div>
|
||||||
|
|
||||||
const { system: sys, cpu, memory: mem } = hw
|
const { system: sys, board, cpu, memory: mem } = hw
|
||||||
const drives = hw.drives ?? []
|
const drives = hw.drives ?? []
|
||||||
const gpus = hw.gpus ?? []
|
const gpus = hw.gpus ?? []
|
||||||
const capacity = drives.reduce((n, d) => n + d.size, 0)
|
const capacity = drives.reduce((n, d) => n + d.size, 0)
|
||||||
@@ -191,14 +243,13 @@ export default function Hardware() {
|
|||||||
{gpus.length === 0 && <div className="empty">No display adapter found.</div>}
|
{gpus.length === 0 && <div className="empty">No display adapter found.</div>}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<Motherboard b={board} />
|
||||||
|
|
||||||
<section className="card">
|
<section className="card">
|
||||||
<h2>System</h2>
|
<h2>System</h2>
|
||||||
<div className="props">
|
<div className="props">
|
||||||
<P k="Model" v={sys.model} />
|
<P k="Model" v={sys.model} />
|
||||||
<P k="Board" v={sys.board} />
|
<P k="Family" v={sys.family} />
|
||||||
<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="Serial" v={sys.serial} mono />
|
||||||
<P k="Kernel" v={sys.kernel} mono />
|
<P k="Kernel" v={sys.kernel} mono />
|
||||||
<P k="Arch" v={sys.arch} />
|
<P k="Arch" v={sys.arch} />
|
||||||
|
|||||||
+8
-2
@@ -118,7 +118,13 @@ export interface DockerHealth { configured: boolean; state: string; status?: str
|
|||||||
export interface DockerEvent { t: number; name: string; action: string; image?: string; exit_code?: string; signal?: string; health?: string }
|
export interface DockerEvent { t: number; name: string; action: string; image?: string; exit_code?: string; signal?: string; health?: string }
|
||||||
|
|
||||||
// ---- hardware (the Hardware tab: what the machine is made of) ----
|
// ---- hardware (the Hardware tab: what the machine is made of) ----
|
||||||
export interface HWSystem { model?: string; serial?: string; board?: string; chassis?: string; bios?: string; bios_date?: string; kernel?: string; arch?: string; virt?: string }
|
export interface HWSystem { model?: string; serial?: string; family?: string; kernel?: string; arch?: string; virt?: string }
|
||||||
|
export interface HWSlot { designation: string; type?: string; usage?: string; length?: string; width?: string; bus?: string; device?: string }
|
||||||
|
export interface HWBoard {
|
||||||
|
maker?: string; product?: string; version?: string; serial?: string; asset_tag?: string; type?: string; location?: string
|
||||||
|
chassis?: string; chassis_maker?: string; bios?: string; bios_date?: string; bios_revision?: string; rom_size?: string
|
||||||
|
uefi?: boolean; slots?: HWSlot[]
|
||||||
|
}
|
||||||
export interface HWCache { level: string; size: string }
|
export interface HWCache { level: string; size: string }
|
||||||
export interface HWCPU {
|
export interface HWCPU {
|
||||||
model?: string; vendor?: string; arch?: string; sockets?: number; socket_names?: string[]
|
model?: string; vendor?: string; arch?: string; sockets?: number; socket_names?: string[]
|
||||||
@@ -133,7 +139,7 @@ export interface HWModule {
|
|||||||
export interface HWMemory { total: number; installed?: number; max_capacity?: number; slots?: number; filled?: number; ecc?: string; modules?: HWModule[] }
|
export interface HWMemory { total: number; installed?: number; max_capacity?: number; slots?: number; filled?: number; ecc?: string; modules?: HWModule[] }
|
||||||
export interface HWDrive { name: string; kind: string; model?: string; vendor?: string; serial?: string; firmware?: string; tran?: string; size: number; rotational?: boolean; removable?: boolean }
|
export interface HWDrive { name: string; kind: string; model?: string; vendor?: string; serial?: string; firmware?: string; tran?: string; size: number; rotational?: boolean; removable?: boolean }
|
||||||
export interface HWGPU { model?: string; vendor?: string; slot?: string; driver?: string; node?: string; memory?: number; class?: string }
|
export interface HWGPU { model?: string; vendor?: string; slot?: string; driver?: string; node?: string; memory?: number; class?: string }
|
||||||
export interface Hardware { at: string; system: HWSystem; cpu: HWCPU; memory: HWMemory; drives: HWDrive[] | null; gpus: HWGPU[] | null; notes?: string[] }
|
export interface Hardware { at: string; system: HWSystem; board: HWBoard; cpu: HWCPU; memory: HWMemory; drives: HWDrive[] | null; gpus: HWGPU[] | null; notes?: string[] }
|
||||||
|
|
||||||
export interface DockerLogLine { t: string; m: string; err?: boolean }
|
export interface DockerLogLine { t: string; m: string; err?: boolean }
|
||||||
export interface DockerInfo { available: boolean; message?: string; tool?: string; containers: DockerContainer[]; images: Record<string, unknown>[]; volumes: Record<string, unknown>[]; networks: Record<string, unknown>[] }
|
export interface DockerInfo { available: boolean; message?: string; tool?: string; containers: DockerContainer[]; images: Record<string, unknown>[]; volumes: Record<string, unknown>[]; networks: Record<string, unknown>[] }
|
||||||
|
|||||||
Reference in New Issue
Block a user