Import beads' UX design system into gastown: - Add internal/ui/ package with Ayu theme colors and semantic styling - styles.go: AdaptiveColor definitions for light/dark mode - terminal.go: TTY detection, NO_COLOR/CLICOLOR support - markdown.go: Glamour rendering with agent mode bypass - pager.go: Smart paging with GT_PAGER support - Add colorized help output (internal/cmd/help.go) - Group headers in accent color - Command names styled for scannability - Flag types and defaults muted - Add gt thanks command (internal/cmd/thanks.go) - Contributor display with same logic as bd thanks - Styled with Ayu theme colors - Update gt doctor to match bd doctor UX - Category grouping (Core, Infrastructure, Rig, Patrol, etc.) - Semantic icons (✓ ⚠ ✖) with Ayu colors - Tree connectors for detail lines - Summary line with pass/warn/fail counts - Warnings section at end with numbered issues - Migrate existing styles to use ui package - internal/style/style.go uses ui.ColorPass etc. - internal/tui/feed/styles.go uses ui package colors Co-Authored-By: SageOx <ox@sageox.ai>
122 lines
3.4 KiB
Go
122 lines
3.4 KiB
Go
package doctor
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"time"
|
|
|
|
"github.com/steveyegge/gastown/internal/boot"
|
|
)
|
|
|
|
// BootHealthCheck verifies Boot watchdog health.
|
|
// "The vet checks on the dog."
|
|
type BootHealthCheck struct {
|
|
BaseCheck
|
|
}
|
|
|
|
// NewBootHealthCheck creates a new Boot health check.
|
|
func NewBootHealthCheck() *BootHealthCheck {
|
|
return &BootHealthCheck{
|
|
BaseCheck: BaseCheck{
|
|
CheckName: "boot-health",
|
|
CheckDescription: "Check Boot watchdog health (the vet checks on the dog)",
|
|
CheckCategory: CategoryInfrastructure,
|
|
},
|
|
}
|
|
}
|
|
|
|
// Run checks Boot health: directory, session, status, and marker freshness.
|
|
func (c *BootHealthCheck) Run(ctx *CheckContext) *CheckResult {
|
|
b := boot.New(ctx.TownRoot)
|
|
details := []string{}
|
|
|
|
// Check 1: Boot directory exists
|
|
bootDir := b.Dir()
|
|
if _, err := os.Stat(bootDir); os.IsNotExist(err) {
|
|
return &CheckResult{
|
|
Name: c.Name(),
|
|
Status: StatusWarning,
|
|
Message: "Boot directory not present",
|
|
Details: []string{fmt.Sprintf("Expected: %s", bootDir)},
|
|
FixHint: "Boot directory is created on first daemon run",
|
|
}
|
|
}
|
|
|
|
// Check 2: Session alive
|
|
sessionAlive := b.IsSessionAlive()
|
|
if sessionAlive {
|
|
details = append(details, fmt.Sprintf("Session: %s (alive)", boot.SessionName))
|
|
} else {
|
|
details = append(details, fmt.Sprintf("Session: %s (not running)", boot.SessionName))
|
|
}
|
|
|
|
// Check 3: Last execution status
|
|
status, err := b.LoadStatus()
|
|
if err != nil {
|
|
return &CheckResult{
|
|
Name: c.Name(),
|
|
Status: StatusError,
|
|
Message: "Failed to load Boot status",
|
|
Details: []string{err.Error()},
|
|
}
|
|
}
|
|
|
|
if !status.CompletedAt.IsZero() {
|
|
age := time.Since(status.CompletedAt).Round(time.Second)
|
|
details = append(details, fmt.Sprintf("Last run: %s ago", age))
|
|
if status.LastAction != "" {
|
|
details = append(details, fmt.Sprintf("Last action: %s", status.LastAction))
|
|
}
|
|
if status.Target != "" {
|
|
details = append(details, fmt.Sprintf("Target: %s", status.Target))
|
|
}
|
|
if status.Error != "" {
|
|
details = append(details, fmt.Sprintf("Last error: %s", status.Error))
|
|
return &CheckResult{
|
|
Name: c.Name(),
|
|
Status: StatusWarning,
|
|
Message: "Boot last run had an error",
|
|
Details: details,
|
|
FixHint: "Check daemon logs for details",
|
|
}
|
|
}
|
|
} else if status.StartedAt.IsZero() {
|
|
details = append(details, "No previous run recorded")
|
|
}
|
|
|
|
// Check 4: Marker file freshness (stale marker indicates crash)
|
|
markerPath := filepath.Join(bootDir, boot.MarkerFileName)
|
|
if info, err := os.Stat(markerPath); err == nil {
|
|
age := time.Since(info.ModTime())
|
|
if age > boot.DefaultMarkerTTL {
|
|
return &CheckResult{
|
|
Name: c.Name(),
|
|
Status: StatusWarning,
|
|
Message: "Boot marker is stale (possible crash)",
|
|
Details: []string{
|
|
fmt.Sprintf("Marker age: %s", age.Round(time.Second)),
|
|
fmt.Sprintf("TTL: %s", boot.DefaultMarkerTTL),
|
|
},
|
|
FixHint: "Stale marker will be cleaned on next daemon tick",
|
|
}
|
|
}
|
|
// Marker exists and is fresh - Boot is currently running
|
|
details = append(details, fmt.Sprintf("Currently running (marker age: %s)", age.Round(time.Second)))
|
|
}
|
|
|
|
// All checks passed
|
|
message := "Boot watchdog healthy"
|
|
if b.IsDegraded() {
|
|
message = "Boot watchdog healthy (degraded mode)"
|
|
details = append(details, "Running in degraded mode (no tmux)")
|
|
}
|
|
|
|
return &CheckResult{
|
|
Name: c.Name(),
|
|
Status: StatusOK,
|
|
Message: message,
|
|
Details: details,
|
|
}
|
|
}
|