feat: implement server-wide management actions and enhance TUI dashboard
This commit is contained in:
@@ -16,5 +16,13 @@ type MsgActionDone struct {
|
||||
// MsgLogsLoaded carries log lines for the selected agent.
|
||||
type MsgLogsLoaded struct{ Lines []string }
|
||||
|
||||
// MsgServerActionDone reports the result of a server-wide bulk action.
|
||||
type MsgServerActionDone struct {
|
||||
Action string
|
||||
Total int
|
||||
Failed int
|
||||
Errors []string
|
||||
}
|
||||
|
||||
// MsgTick triggers a periodic refresh.
|
||||
type MsgTick struct{}
|
||||
|
||||
+17
-4
@@ -10,6 +10,7 @@ const (
|
||||
ScreenAgentList // list all agents with status
|
||||
ScreenAgentActions // actions for a selected agent
|
||||
ScreenLogs // tail log output
|
||||
ScreenServer // server-wide process management
|
||||
)
|
||||
|
||||
// Model is the complete TUI state — pure data.
|
||||
@@ -34,10 +35,11 @@ type AgentView struct {
|
||||
Enabled bool
|
||||
Running bool
|
||||
PID int
|
||||
Uptime string // formatted: "2h 15m"
|
||||
Memory string // formatted: "42 MB"
|
||||
CPU string // formatted: "1.2%"
|
||||
LogSize string // formatted: "350 KB"
|
||||
Instances int // number of running instances (>1 means duplicates)
|
||||
Uptime string // formatted: "2h 15m"
|
||||
Memory string // formatted: "42 MB"
|
||||
CPU string // formatted: "1.2%"
|
||||
LogSize string // formatted: "350 KB"
|
||||
}
|
||||
|
||||
// MenuOption represents a selectable menu item.
|
||||
@@ -50,10 +52,21 @@ type MenuOption struct {
|
||||
func MainMenuOptions() []MenuOption {
|
||||
return []MenuOption{
|
||||
{Label: "Agents", Desc: "Gestionar agentes"},
|
||||
{Label: "Server", Desc: "Gestionar servidor"},
|
||||
{Label: "Quit", Desc: "Salir"},
|
||||
}
|
||||
}
|
||||
|
||||
// ServerMenuOptions returns the available server-wide actions.
|
||||
func ServerMenuOptions() []MenuOption {
|
||||
return []MenuOption{
|
||||
{Label: "Start All", Desc: "Iniciar todos los agentes habilitados"},
|
||||
{Label: "Stop All", Desc: "Detener todos los agentes"},
|
||||
{Label: "Restart All", Desc: "Reiniciar todos los agentes"},
|
||||
{Label: "Kill All", Desc: "SIGKILL forzado a todos"},
|
||||
}
|
||||
}
|
||||
|
||||
// AgentActionOptions returns the available actions based on agent state.
|
||||
func AgentActionOptions(running bool) []MenuOption {
|
||||
if running {
|
||||
|
||||
+64
-3
@@ -14,6 +14,12 @@ const (
|
||||
IntentLoadLogs IntentKind = "load_logs"
|
||||
IntentTick IntentKind = "tick"
|
||||
IntentQuit IntentKind = "quit"
|
||||
|
||||
// Server-wide bulk operations
|
||||
IntentStartAll IntentKind = "start_all"
|
||||
IntentStopAll IntentKind = "stop_all"
|
||||
IntentRestartAll IntentKind = "restart_all"
|
||||
IntentKillAll IntentKind = "kill_all"
|
||||
)
|
||||
|
||||
// Intent is pure data describing a side effect to execute.
|
||||
@@ -45,9 +51,11 @@ func Update(model Model, msg interface{}) (Model, []Intent) {
|
||||
|
||||
case MsgAgentsLoaded:
|
||||
model.Agents = m.Agents
|
||||
// Clamp cursor
|
||||
if model.Cursor >= len(model.Agents) && len(model.Agents) > 0 {
|
||||
model.Cursor = len(model.Agents) - 1
|
||||
// Clamp cursor only on screens that use the agent list
|
||||
if model.Screen == ScreenAgentList {
|
||||
if model.Cursor >= len(model.Agents) && len(model.Agents) > 0 {
|
||||
model.Cursor = len(model.Agents) - 1
|
||||
}
|
||||
}
|
||||
return model, []Intent{{Kind: IntentTick}}
|
||||
|
||||
@@ -59,6 +67,14 @@ func Update(model Model, msg interface{}) (Model, []Intent) {
|
||||
}
|
||||
return model, []Intent{{Kind: IntentLoadAgents}}
|
||||
|
||||
case MsgServerActionDone:
|
||||
if m.Failed == 0 {
|
||||
model.StatusMsg = fmt.Sprintf("%s: %d agents OK", m.Action, m.Total)
|
||||
} else {
|
||||
model.StatusMsg = fmt.Sprintf("%s: %d/%d failed", m.Action, m.Failed, m.Total)
|
||||
}
|
||||
return model, []Intent{{Kind: IntentLoadAgents}}
|
||||
|
||||
case MsgLogsLoaded:
|
||||
model.LogLines = m.Lines
|
||||
model.LogScroll = max(0, len(m.Lines)-visibleLogLines(model))
|
||||
@@ -92,6 +108,8 @@ func updateKey(model Model, key KeyMsg) (Model, []Intent) {
|
||||
return updateAgentActions(model, key)
|
||||
case ScreenLogs:
|
||||
return updateLogs(model, key)
|
||||
case ScreenServer:
|
||||
return updateServerScreen(model, key)
|
||||
}
|
||||
return model, nil
|
||||
}
|
||||
@@ -109,6 +127,11 @@ func updateMainScreen(model Model, key KeyMsg) (Model, []Intent) {
|
||||
model.Screen = ScreenAgentList
|
||||
model.Cursor = 0
|
||||
return model, []Intent{{Kind: IntentLoadAgents}}
|
||||
case "Server":
|
||||
model.Screen = ScreenServer
|
||||
model.Cursor = 0
|
||||
model.StatusMsg = ""
|
||||
return model, []Intent{{Kind: IntentLoadAgents}}
|
||||
case "Quit":
|
||||
return model, []Intent{{Kind: IntentQuit}}
|
||||
}
|
||||
@@ -210,6 +233,44 @@ func updateLogs(model Model, key KeyMsg) (Model, []Intent) {
|
||||
return model, nil
|
||||
}
|
||||
|
||||
func updateServerScreen(model Model, key KeyMsg) (Model, []Intent) {
|
||||
opts := ServerMenuOptions()
|
||||
|
||||
switch key.Str {
|
||||
case "0":
|
||||
model.Screen = ScreenMain
|
||||
model.Cursor = 0
|
||||
model.StatusMsg = ""
|
||||
case "up", "k":
|
||||
model.Cursor = clamp(model.Cursor-1, 0, len(opts)-1)
|
||||
case "down", "j":
|
||||
model.Cursor = clamp(model.Cursor+1, 0, len(opts)-1)
|
||||
case "enter":
|
||||
if model.Cursor < len(opts) {
|
||||
return executeServerAction(model, opts[model.Cursor].Label)
|
||||
}
|
||||
}
|
||||
return model, nil
|
||||
}
|
||||
|
||||
func executeServerAction(model Model, action string) (Model, []Intent) {
|
||||
switch action {
|
||||
case "Start All":
|
||||
model.StatusMsg = "Starting all agents..."
|
||||
return model, []Intent{{Kind: IntentStartAll}}
|
||||
case "Stop All":
|
||||
model.StatusMsg = "Stopping all agents..."
|
||||
return model, []Intent{{Kind: IntentStopAll}}
|
||||
case "Restart All":
|
||||
model.StatusMsg = "Restarting all agents..."
|
||||
return model, []Intent{{Kind: IntentRestartAll}}
|
||||
case "Kill All":
|
||||
model.StatusMsg = "Killing all agents..."
|
||||
return model, []Intent{{Kind: IntentKillAll}}
|
||||
}
|
||||
return model, nil
|
||||
}
|
||||
|
||||
// ── pure helpers ─────────────────────────────────────────────────────────
|
||||
|
||||
func visibleLogLines(m Model) int {
|
||||
|
||||
@@ -16,6 +16,8 @@ func View(model Model) string {
|
||||
return viewAgentActions(model)
|
||||
case ScreenLogs:
|
||||
return viewLogs(model)
|
||||
case ScreenServer:
|
||||
return viewServer(model)
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
@@ -78,6 +80,10 @@ func viewAgentList(m Model) string {
|
||||
|
||||
b.WriteString(fmt.Sprintf(" %s%s %-20s %-8s %s\n",
|
||||
cursor, icon, a.ID, a.Version, status))
|
||||
|
||||
if a.Instances > 1 {
|
||||
b.WriteString(fmt.Sprintf(" ⚠ WARNING: %d instances running!\n", a.Instances))
|
||||
}
|
||||
}
|
||||
|
||||
if m.StatusMsg != "" {
|
||||
@@ -177,6 +183,54 @@ func viewLogs(m Model) string {
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func viewServer(m Model) string {
|
||||
var b strings.Builder
|
||||
|
||||
b.WriteString("\n Server Management\n")
|
||||
b.WriteString(" " + strings.Repeat("─", 44) + "\n")
|
||||
|
||||
// Summary
|
||||
running, stopped, disabled := countStatuses(m.Agents)
|
||||
total := len(m.Agents)
|
||||
if total > 0 {
|
||||
b.WriteString(fmt.Sprintf(" %d agents: %d running, %d stopped, %d disabled\n", total, running, stopped, disabled))
|
||||
} else {
|
||||
b.WriteString(" Loading...\n")
|
||||
}
|
||||
|
||||
// Agent status list (compact)
|
||||
if total > 0 {
|
||||
b.WriteString("\n")
|
||||
for _, a := range m.Agents {
|
||||
icon := "○"
|
||||
if !a.Enabled {
|
||||
icon = " "
|
||||
} else if a.Running {
|
||||
icon = "●"
|
||||
}
|
||||
b.WriteString(fmt.Sprintf(" %s %s\n", icon, a.ID))
|
||||
}
|
||||
}
|
||||
|
||||
b.WriteString("\n")
|
||||
|
||||
// Action menu
|
||||
for i, opt := range ServerMenuOptions() {
|
||||
cursor := " "
|
||||
if i == m.Cursor {
|
||||
cursor = "> "
|
||||
}
|
||||
b.WriteString(fmt.Sprintf(" %s%-16s %s\n", cursor, opt.Label, opt.Desc))
|
||||
}
|
||||
|
||||
if m.StatusMsg != "" {
|
||||
b.WriteString("\n " + m.StatusMsg + "\n")
|
||||
}
|
||||
|
||||
b.WriteString("\n ↑↓ navegar enter ejecutar 0 volver\n")
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func countStatuses(agents []AgentView) (running, stopped, disabled int) {
|
||||
for _, a := range agents {
|
||||
switch {
|
||||
|
||||
Reference in New Issue
Block a user