BMC: - collector/board.go: collectBMCFirmware() via ipmitool mc info, graceful skip if /dev/ipmi0 absent - collector/collector.go: append BMC firmware record to snap.Firmware - app/panel.go: show BMC version in TUI right-panel header alongside BIOS CPU SAT: - platform/sat.go: RunCPUAcceptancePack(baseDir, durationSec) — lscpu + sensors before/after + stress-ng - app/app.go: RunCPUAcceptancePack + RunCPUAcceptancePackResult methods, satRunner interface updated - app/panel.go: CPU row now reads real PASS/FAIL from cpu-*/summary.txt via satStatuses(); cpuDetailResult shows last SAT summary + audit data - tui/types.go: actionRunCPUSAT, confirmBody for CPU test with mode label - tui/screen_health_check.go: hcCPUDurations [60,300,900]s; hcRunSingle(CPU)→confirm screen; executeRunAll uses RunCPUAcceptancePackResult - tui/forms.go: actionRunCPUSAT → RunCPUAcceptancePackResult with mode duration - cmd/bee/main.go: bee sat cpu [--duration N] subcommand Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
292 lines
6.8 KiB
Go
292 lines
6.8 KiB
Go
package tui
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
|
|
tea "github.com/charmbracelet/bubbletea"
|
|
)
|
|
|
|
// Component indices.
|
|
const (
|
|
hcGPU = 0
|
|
hcMemory = 1
|
|
hcStorage = 2
|
|
hcCPU = 3
|
|
)
|
|
|
|
// Cursor positions in Health Check screen.
|
|
const (
|
|
hcCurGPU = 0
|
|
hcCurMemory = 1
|
|
hcCurStorage = 2
|
|
hcCurCPU = 3
|
|
hcCurSelectAll = 4
|
|
hcCurModeQuick = 5
|
|
hcCurModeStd = 6
|
|
hcCurModeExpr = 7
|
|
hcCurRunAll = 8
|
|
hcCurTotal = 9
|
|
)
|
|
|
|
// hcModeDurations maps mode index (0=Quick,1=Standard,2=Express) to GPU stress seconds.
|
|
var hcModeDurations = [3]int{600, 3600, 28800}
|
|
|
|
// hcCPUDurations maps mode index to CPU stress-ng seconds.
|
|
var hcCPUDurations = [3]int{60, 300, 900}
|
|
|
|
func (m model) enterHealthCheck() (tea.Model, tea.Cmd) {
|
|
m.screen = screenHealthCheck
|
|
if !m.hcInitialized {
|
|
m.hcSel = [4]bool{true, true, true, true}
|
|
m.hcMode = 0
|
|
m.hcCursor = 0
|
|
m.hcInitialized = true
|
|
}
|
|
return m, nil
|
|
}
|
|
|
|
func (m model) updateHealthCheck(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
|
switch msg.String() {
|
|
case "up", "k":
|
|
if m.hcCursor > 0 {
|
|
m.hcCursor--
|
|
}
|
|
case "down", "j":
|
|
if m.hcCursor < hcCurTotal-1 {
|
|
m.hcCursor++
|
|
}
|
|
case " ":
|
|
switch m.hcCursor {
|
|
case hcCurGPU, hcCurMemory, hcCurStorage, hcCurCPU:
|
|
m.hcSel[m.hcCursor] = !m.hcSel[m.hcCursor]
|
|
case hcCurSelectAll:
|
|
allOn := m.hcSel[0] && m.hcSel[1] && m.hcSel[2] && m.hcSel[3]
|
|
for i := range m.hcSel {
|
|
m.hcSel[i] = !allOn
|
|
}
|
|
case hcCurModeQuick, hcCurModeStd, hcCurModeExpr:
|
|
m.hcMode = m.hcCursor - hcCurModeQuick
|
|
}
|
|
case "enter":
|
|
switch m.hcCursor {
|
|
case hcCurGPU, hcCurMemory, hcCurStorage, hcCurCPU:
|
|
return m.hcRunSingle(m.hcCursor)
|
|
case hcCurSelectAll:
|
|
allOn := m.hcSel[0] && m.hcSel[1] && m.hcSel[2] && m.hcSel[3]
|
|
for i := range m.hcSel {
|
|
m.hcSel[i] = !allOn
|
|
}
|
|
case hcCurModeQuick, hcCurModeStd, hcCurModeExpr:
|
|
m.hcMode = m.hcCursor - hcCurModeQuick
|
|
case hcCurRunAll:
|
|
return m.hcRunAll()
|
|
}
|
|
case "g", "G":
|
|
return m.hcRunSingle(hcGPU)
|
|
case "m", "M":
|
|
return m.hcRunSingle(hcMemory)
|
|
case "s", "S":
|
|
return m.hcRunSingle(hcStorage)
|
|
case "c", "C":
|
|
return m.hcRunSingle(hcCPU)
|
|
case "r", "R":
|
|
return m.hcRunAll()
|
|
case "a", "A":
|
|
allOn := m.hcSel[0] && m.hcSel[1] && m.hcSel[2] && m.hcSel[3]
|
|
for i := range m.hcSel {
|
|
m.hcSel[i] = !allOn
|
|
}
|
|
case "1":
|
|
m.hcMode = 0
|
|
case "2":
|
|
m.hcMode = 1
|
|
case "3":
|
|
m.hcMode = 2
|
|
case "esc":
|
|
m.screen = screenMain
|
|
m.cursor = 0
|
|
case "q", "ctrl+c":
|
|
return m, tea.Quit
|
|
}
|
|
return m, nil
|
|
}
|
|
|
|
func (m model) hcRunSingle(idx int) (tea.Model, tea.Cmd) {
|
|
switch idx {
|
|
case hcGPU:
|
|
m.nvidiaDurIdx = m.hcMode
|
|
return m.enterNvidiaSATSetup()
|
|
case hcMemory:
|
|
m.pendingAction = actionRunMemorySAT
|
|
m.screen = screenConfirm
|
|
m.cursor = 0
|
|
return m, nil
|
|
case hcStorage:
|
|
m.pendingAction = actionRunStorageSAT
|
|
m.screen = screenConfirm
|
|
m.cursor = 0
|
|
return m, nil
|
|
case hcCPU:
|
|
m.pendingAction = actionRunCPUSAT
|
|
m.screen = screenConfirm
|
|
m.cursor = 0
|
|
return m, nil
|
|
}
|
|
return m, nil
|
|
}
|
|
|
|
func (m model) hcRunAll() (tea.Model, tea.Cmd) {
|
|
for _, sel := range m.hcSel {
|
|
if sel {
|
|
m.pendingAction = actionRunAll
|
|
m.screen = screenConfirm
|
|
m.cursor = 0
|
|
return m, nil
|
|
}
|
|
}
|
|
return m, nil
|
|
}
|
|
|
|
func (m model) executeRunAll() (tea.Model, tea.Cmd) {
|
|
durationSec := hcModeDurations[m.hcMode]
|
|
durationIdx := m.hcMode
|
|
sel := m.hcSel
|
|
app := m.app
|
|
m.busy = true
|
|
m.busyTitle = "Health Check"
|
|
return m, func() tea.Msg {
|
|
var parts []string
|
|
if sel[hcGPU] {
|
|
gpus, err := app.ListNvidiaGPUs()
|
|
if err != nil || len(gpus) == 0 {
|
|
parts = append(parts, "=== GPU ===\nNo NVIDIA GPUs detected or driver not loaded.")
|
|
} else {
|
|
var indices []int
|
|
sizeMB := 0
|
|
for _, g := range gpus {
|
|
indices = append(indices, g.Index)
|
|
if sizeMB == 0 || g.MemoryMB < sizeMB {
|
|
sizeMB = g.MemoryMB
|
|
}
|
|
}
|
|
if sizeMB == 0 {
|
|
sizeMB = 64
|
|
}
|
|
r, err := app.RunNvidiaAcceptancePackWithOptions(context.Background(), "", durationSec, sizeMB, indices)
|
|
body := r.Body
|
|
if err != nil {
|
|
body += "\nERROR: " + err.Error()
|
|
}
|
|
parts = append(parts, "=== GPU ===\n"+body)
|
|
}
|
|
}
|
|
if sel[hcMemory] {
|
|
r, err := app.RunMemoryAcceptancePackResult("")
|
|
body := r.Body
|
|
if err != nil {
|
|
body += "\nERROR: " + err.Error()
|
|
}
|
|
parts = append(parts, "=== MEMORY ===\n"+body)
|
|
}
|
|
if sel[hcStorage] {
|
|
r, err := app.RunStorageAcceptancePackResult("")
|
|
body := r.Body
|
|
if err != nil {
|
|
body += "\nERROR: " + err.Error()
|
|
}
|
|
parts = append(parts, "=== STORAGE ===\n"+body)
|
|
}
|
|
if sel[hcCPU] {
|
|
cpuDur := hcCPUDurations[durationIdx]
|
|
r, err := app.RunCPUAcceptancePackResult("", cpuDur)
|
|
body := r.Body
|
|
if err != nil {
|
|
body += "\nERROR: " + err.Error()
|
|
}
|
|
parts = append(parts, "=== CPU ===\n"+body)
|
|
}
|
|
combined := strings.Join(parts, "\n\n")
|
|
if combined == "" {
|
|
combined = "No components selected."
|
|
}
|
|
return resultMsg{title: "Health Check", body: combined, back: screenHealthCheck}
|
|
}
|
|
}
|
|
|
|
func renderHealthCheck(m model) string {
|
|
var b strings.Builder
|
|
|
|
fmt.Fprintln(&b, "HEALTH CHECK")
|
|
fmt.Fprintln(&b)
|
|
fmt.Fprintln(&b, " Diagnostics:")
|
|
fmt.Fprintln(&b)
|
|
|
|
type comp struct{ name, desc, key string }
|
|
comps := []comp{
|
|
{"GPU", "nvidia-smi + bee-gpu-stress", "G"},
|
|
{"MEMORY", "memtester", "M"},
|
|
{"STORAGE", "smartctl + NVMe self-test", "S"},
|
|
{"CPU", "audit diagnostics", "C"},
|
|
}
|
|
for i, c := range comps {
|
|
pfx := " "
|
|
if m.hcCursor == i {
|
|
pfx = "> "
|
|
}
|
|
ch := "[ ]"
|
|
if m.hcSel[i] {
|
|
ch = "[x]"
|
|
}
|
|
fmt.Fprintf(&b, "%s%s %-8s %-28s [%s]\n", pfx, ch, c.name, c.desc, c.key)
|
|
}
|
|
|
|
fmt.Fprintln(&b, " ─────────────────────────────────────────────────")
|
|
{
|
|
pfx := " "
|
|
if m.hcCursor == hcCurSelectAll {
|
|
pfx = "> "
|
|
}
|
|
allOn := m.hcSel[0] && m.hcSel[1] && m.hcSel[2] && m.hcSel[3]
|
|
ch := "[ ]"
|
|
if allOn {
|
|
ch = "[x]"
|
|
}
|
|
fmt.Fprintf(&b, "%s%s Select / Deselect All [A]\n", pfx, ch)
|
|
}
|
|
|
|
fmt.Fprintln(&b)
|
|
fmt.Fprintln(&b, " Mode:")
|
|
modes := []struct{ label, key string }{
|
|
{"Quick", "1"},
|
|
{"Standard", "2"},
|
|
{"Express", "3"},
|
|
}
|
|
for i, mode := range modes {
|
|
pfx := " "
|
|
if m.hcCursor == hcCurModeQuick+i {
|
|
pfx = "> "
|
|
}
|
|
radio := "( )"
|
|
if m.hcMode == i {
|
|
radio = "(*)"
|
|
}
|
|
fmt.Fprintf(&b, "%s%s %-10s [%s]\n", pfx, radio, mode.label, mode.key)
|
|
}
|
|
|
|
fmt.Fprintln(&b)
|
|
{
|
|
pfx := " "
|
|
if m.hcCursor == hcCurRunAll {
|
|
pfx = "> "
|
|
}
|
|
fmt.Fprintf(&b, "%s[ RUN ALL [R] ]\n", pfx)
|
|
}
|
|
|
|
fmt.Fprintln(&b)
|
|
fmt.Fprintln(&b, "─────────────────────────────────────────────────────────────────")
|
|
fmt.Fprint(&b, "[↑↓] move [space/enter] toggle [letter] single test [R] run all [Esc] back")
|
|
return b.String()
|
|
}
|