- Replace 12-item flat menu with 4-item main menu: Health Check, Export support bundle, Settings, Exit - Add Health Check screen (Lenovo-style): per-component checkboxes (GPU/MEM/DISK/CPU), Quick/Standard/Express modes, Run All, letter hotkeys G/M/S/C/R/A/1/2/3 - Add two-column main screen: left = menu, right = hardware panel with colored PASS/FAIL/CANCEL/N/A status per component; Tab/→ switches focus, Enter opens component detail - Add app.LoadHardwarePanel() + ComponentDetailResult() reading audit JSON and SAT summary.txt files - Move Network/Services/audit actions into Settings submenu - Export: support bundle only (remove separate audit JSON export) - Delete screen_acceptance.go; add screen_health_check.go, screen_settings.go, app/panel.go - Add BMC + CPU stress-ng tests to backlog - Update bible submodule - Rewrite tui_test.go for new screen/action structure Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
246 lines
5.6 KiB
Go
246 lines
5.6 KiB
Go
package tui
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
tea "github.com/charmbracelet/bubbletea"
|
|
)
|
|
|
|
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|
switch msg := msg.(type) {
|
|
case tea.KeyMsg:
|
|
if m.busy {
|
|
if msg.String() == "ctrl+c" {
|
|
return m, tea.Quit
|
|
}
|
|
return m, nil
|
|
}
|
|
return m.updateKey(msg)
|
|
case resultMsg:
|
|
m.busy = false
|
|
m.busyTitle = ""
|
|
m.title = msg.title
|
|
if msg.err != nil {
|
|
body := strings.TrimSpace(msg.body)
|
|
if body == "" {
|
|
m.body = fmt.Sprintf("ERROR: %v", msg.err)
|
|
} else {
|
|
m.body = fmt.Sprintf("%s\n\nERROR: %v", body, msg.err)
|
|
}
|
|
} else {
|
|
m.body = msg.body
|
|
}
|
|
m.pendingAction = actionNone
|
|
if msg.back != "" {
|
|
m.prevScreen = msg.back
|
|
} else {
|
|
m.prevScreen = m.screen
|
|
}
|
|
m.screen = screenOutput
|
|
m.cursor = 0
|
|
return m, nil
|
|
case servicesMsg:
|
|
m.busy = false
|
|
m.busyTitle = ""
|
|
if msg.err != nil {
|
|
m.title = "Services"
|
|
m.body = msg.err.Error()
|
|
m.prevScreen = screenSettings
|
|
m.screen = screenOutput
|
|
return m, nil
|
|
}
|
|
m.services = msg.services
|
|
m.screen = screenServices
|
|
m.cursor = 0
|
|
return m, nil
|
|
case interfacesMsg:
|
|
m.busy = false
|
|
m.busyTitle = ""
|
|
if msg.err != nil {
|
|
m.title = "interfaces"
|
|
m.body = msg.err.Error()
|
|
m.prevScreen = screenNetwork
|
|
m.screen = screenOutput
|
|
return m, nil
|
|
}
|
|
m.interfaces = msg.ifaces
|
|
m.screen = screenInterfacePick
|
|
m.cursor = 0
|
|
return m, nil
|
|
case exportTargetsMsg:
|
|
m.busy = false
|
|
m.busyTitle = ""
|
|
if msg.err != nil {
|
|
m.title = "export"
|
|
m.body = msg.err.Error()
|
|
m.prevScreen = screenMain
|
|
m.screen = screenOutput
|
|
return m, nil
|
|
}
|
|
m.targets = msg.targets
|
|
m.screen = screenExportTargets
|
|
m.cursor = 0
|
|
return m, nil
|
|
case panelMsg:
|
|
m.panel = msg.data
|
|
return m, nil
|
|
case nvidiaGPUsMsg:
|
|
return m.handleNvidiaGPUsMsg(msg)
|
|
case nvtopClosedMsg:
|
|
return m, nil
|
|
case nvidiaSATDoneMsg:
|
|
if m.nvidiaSATAborted {
|
|
return m, nil
|
|
}
|
|
if m.nvidiaSATCancel != nil {
|
|
m.nvidiaSATCancel()
|
|
m.nvidiaSATCancel = nil
|
|
}
|
|
m.prevScreen = screenHealthCheck
|
|
m.screen = screenOutput
|
|
m.title = msg.title
|
|
if msg.err != nil {
|
|
body := strings.TrimSpace(msg.body)
|
|
if body == "" {
|
|
m.body = fmt.Sprintf("ERROR: %v", msg.err)
|
|
} else {
|
|
m.body = fmt.Sprintf("%s\n\nERROR: %v", body, msg.err)
|
|
}
|
|
} else {
|
|
m.body = msg.body
|
|
}
|
|
return m, nil
|
|
}
|
|
return m, nil
|
|
}
|
|
|
|
func (m model) updateKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
|
switch m.screen {
|
|
case screenMain:
|
|
return m.updateMain(msg)
|
|
case screenHealthCheck:
|
|
return m.updateHealthCheck(msg)
|
|
case screenSettings:
|
|
return m.updateMenu(msg, len(m.settingsMenu), m.handleSettingsMenu)
|
|
case screenNetwork:
|
|
return m.updateMenu(msg, len(m.networkMenu), m.handleNetworkMenu)
|
|
case screenServices:
|
|
return m.updateMenu(msg, len(m.services), m.handleServicesMenu)
|
|
case screenServiceAction:
|
|
return m.updateMenu(msg, len(m.serviceMenu), m.handleServiceActionMenu)
|
|
case screenNvidiaSATSetup:
|
|
return m.updateNvidiaSATSetup(msg)
|
|
case screenNvidiaSATRunning:
|
|
return m.updateNvidiaSATRunning(msg)
|
|
case screenExportTargets:
|
|
return m.updateMenu(msg, len(m.targets), m.handleExportTargetsMenu)
|
|
case screenInterfacePick:
|
|
return m.updateMenu(msg, len(m.interfaces), m.handleInterfacePickMenu)
|
|
case screenOutput:
|
|
switch msg.String() {
|
|
case "esc", "enter", "q":
|
|
m.screen = m.prevScreen
|
|
m.body = ""
|
|
m.title = ""
|
|
m.pendingAction = actionNone
|
|
// Refresh panel when returning to main screen.
|
|
if m.prevScreen == screenMain {
|
|
return m, func() tea.Msg { return panelMsg{data: m.app.LoadHardwarePanel()} }
|
|
}
|
|
return m, nil
|
|
case "ctrl+c":
|
|
return m, tea.Quit
|
|
}
|
|
case screenStaticForm:
|
|
return m.updateStaticForm(msg)
|
|
case screenConfirm:
|
|
return m.updateConfirm(msg)
|
|
}
|
|
if msg.String() == "ctrl+c" {
|
|
return m, tea.Quit
|
|
}
|
|
return m, nil
|
|
}
|
|
|
|
// updateMain handles keys on the main (two-column) screen.
|
|
func (m model) updateMain(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
|
if m.panelFocus {
|
|
return m.updateMainPanel(msg)
|
|
}
|
|
// Switch focus to right panel.
|
|
if (msg.String() == "tab" || msg.String() == "right" || msg.String() == "l") && len(m.panel.Rows) > 0 {
|
|
m.panelFocus = true
|
|
return m, nil
|
|
}
|
|
return m.updateMenu(msg, len(m.mainMenu), m.handleMainMenu)
|
|
}
|
|
|
|
// updateMainPanel handles keys when right panel has focus.
|
|
func (m model) updateMainPanel(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
|
switch msg.String() {
|
|
case "up", "k":
|
|
if m.panelCursor > 0 {
|
|
m.panelCursor--
|
|
}
|
|
case "down", "j":
|
|
if m.panelCursor < len(m.panel.Rows)-1 {
|
|
m.panelCursor++
|
|
}
|
|
case "enter":
|
|
if m.panelCursor < len(m.panel.Rows) {
|
|
key := m.panel.Rows[m.panelCursor].Key
|
|
m.busy = true
|
|
m.busyTitle = key
|
|
return m, func() tea.Msg {
|
|
r := m.app.ComponentDetailResult(key)
|
|
return resultMsg{title: r.Title, body: r.Body, back: screenMain}
|
|
}
|
|
}
|
|
case "tab", "left", "h", "esc":
|
|
m.panelFocus = false
|
|
case "q", "ctrl+c":
|
|
return m, tea.Quit
|
|
}
|
|
return m, nil
|
|
}
|
|
|
|
func (m model) updateMenu(msg tea.KeyMsg, size int, onEnter func() (tea.Model, tea.Cmd)) (tea.Model, tea.Cmd) {
|
|
if size == 0 {
|
|
size = 1
|
|
}
|
|
switch msg.String() {
|
|
case "up", "k":
|
|
if m.cursor > 0 {
|
|
m.cursor--
|
|
}
|
|
case "down", "j":
|
|
if m.cursor < size-1 {
|
|
m.cursor++
|
|
}
|
|
case "enter":
|
|
return onEnter()
|
|
case "esc":
|
|
switch m.screen {
|
|
case screenNetwork, screenServices:
|
|
m.screen = screenSettings
|
|
m.cursor = 0
|
|
case screenSettings:
|
|
m.screen = screenMain
|
|
m.cursor = 0
|
|
case screenServiceAction:
|
|
m.screen = screenServices
|
|
m.cursor = 0
|
|
case screenExportTargets:
|
|
m.screen = screenMain
|
|
m.cursor = 0
|
|
case screenInterfacePick:
|
|
m.screen = screenNetwork
|
|
m.cursor = 0
|
|
}
|
|
case "q", "ctrl+c":
|
|
return m, tea.Quit
|
|
}
|
|
return m, nil
|
|
}
|