fix(logging): add comprehensive UI event logging and prevent stdout leak to TUI
- commit
- da00894a7fd1fd32fa40a09cb4337e74c3120d19
- author
- bnema · · 2025-10-26 15:31 UTC
- parent
- c0e6952
- signature
- unsigned
3 files · +105 -10
M cmd/archup-installer/main.go +2 -1
| @@ -38,6 +38,7 @@ func main() { | |||
| 38 | 38 | fmt.Fprintf(os.Stderr, "Failed to create logger: %v\n", err) | |
| 39 | 39 | os.Exit(1) | |
| 40 | 40 | } | |
| 41 | + | defer log.Close() | |
| 41 | 42 | ||
| 42 | 43 | // Handle --cleanup flag | |
| 43 | 44 | if *doCleanup { | |
| @@ -79,7 +80,7 @@ func main() { | |||
| 79 | 80 | } | |
| 80 | 81 | ||
| 81 | 82 | // Create UI model | |
| 82 | - | model := ui.NewModel(orchestrator, cfg, version) | |
| 83 | + | model := ui.NewModel(orchestrator, cfg, log, version) | |
| 83 | 84 | ||
| 84 | 85 | // Run Bubbletea app | |
| 85 | 86 | p := tea.NewProgram(model, tea.WithAltScreen()) | |
| @@ -38,6 +38,7 @@ func main() { | |||||
| 38 | fmt.Fprintf(os.Stderr, "Failed to create logger: %v\n", err) | 38 | fmt.Fprintf(os.Stderr, "Failed to create logger: %v\n", err) | ||
| 39 | os.Exit(1) | 39 | os.Exit(1) | ||
| 40 | } | 40 | } | ||
| 41 | + | defer log.Close() | |||
| 41 | 42 | ||||
| 42 | // Handle --cleanup flag | 43 | // Handle --cleanup flag | ||
| 43 | if *doCleanup { | 44 | if *doCleanup { | ||
| @@ -79,7 +80,7 @@ func main() { | |||||
| 79 | } | 80 | } | ||
| 80 | 81 | ||||
| 81 | // Create UI model | 82 | // Create UI model | ||
| 82 | - | model := ui.NewModel(orchestrator, cfg, version) | 83 | + | model := ui.NewModel(orchestrator, cfg, log, version) |
| 83 | 84 | ||||
| 84 | // Run Bubbletea app | 85 | // Run Bubbletea app | ||
| 85 | p := tea.NewProgram(model, tea.WithAltScreen()) | 86 | p := tea.NewProgram(model, tea.WithAltScreen()) | ||
M internal/logger/logger.go +56 -7
| @@ -2,6 +2,7 @@ package logger | |||
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | 4 | "fmt" | |
| 5 | + | "io" | |
| 5 | 6 | "log/slog" | |
| 6 | 7 | "os" | |
| 7 | 8 | "strings" | |
| @@ -11,30 +12,70 @@ import ( | |||
| 11 | 12 | ||
| 12 | 13 | // Logger wraps slog.Logger and adds dry-run mode support | |
| 13 | 14 | type Logger struct { | |
| 14 | - | slog *slog.Logger | |
| 15 | - | logPath string | |
| 16 | - | dryRun bool | |
| 15 | + | slog *slog.Logger | |
| 16 | + | logPath string | |
| 17 | + | dryRun bool | |
| 18 | + | logFile *os.File | |
| 19 | + | } | |
| 20 | + | ||
| 21 | + | // syncWriter wraps an io.Writer to sync after every write | |
| 22 | + | type syncWriter struct { | |
| 23 | + | w interface { | |
| 24 | + | io.Writer | |
| 25 | + | Sync() error | |
| 26 | + | } | |
| 27 | + | } | |
| 28 | + | ||
| 29 | + | func (sw *syncWriter) Write(p []byte) (n int, err error) { | |
| 30 | + | n, err = sw.w.Write(p) | |
| 31 | + | if err != nil { | |
| 32 | + | return n, err | |
| 33 | + | } | |
| 34 | + | // Sync to disk immediately to ensure logs are written even on crash | |
| 35 | + | if syncErr := sw.w.Sync(); syncErr != nil { | |
| 36 | + | // Log sync error but don't fail the write | |
| 37 | + | fmt.Fprintf(os.Stderr, "Warning: failed to sync log file: %v\n", syncErr) | |
| 38 | + | } | |
| 39 | + | return n, err | |
| 17 | 40 | } | |
| 18 | 41 | ||
| 19 | 42 | // New creates a new Logger with optional file logging and dry-run mode | |
| 20 | 43 | func New(logPath string, dryRun bool) (*Logger, error) { | |
| 21 | - | // Create slog handler | |
| 22 | - | handler := slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{ | |
| 44 | + | // Open log file for writing with sync flag for immediate writes | |
| 45 | + | logFile, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND|os.O_SYNC, 0644) | |
| 46 | + | if err != nil { | |
| 47 | + | return nil, fmt.Errorf("failed to open log file: %w", err) | |
| 48 | + | } | |
| 49 | + | ||
| 50 | + | // Wrap log file with sync writer to ensure immediate disk writes | |
| 51 | + | syncLogFile := &syncWriter{w: logFile} | |
| 52 | + | ||
| 53 | + | // Only write to log file (not stdout) to avoid interfering with TUI | |
| 54 | + | // If you need stdout logging for debugging, add it back temporarily | |
| 55 | + | handler := slog.NewTextHandler(syncLogFile, &slog.HandlerOptions{ | |
| 23 | 56 | Level: slog.LevelInfo, | |
| 24 | 57 | }) | |
| 25 | 58 | ||
| 26 | - | return &Logger{ | |
| 59 | + | logger := &Logger{ | |
| 27 | 60 | slog: slog.New(handler), | |
| 28 | 61 | logPath: logPath, | |
| 62 | + | logFile: logFile, | |
| 29 | 63 | dryRun: dryRun, | |
| 30 | - | }, nil | |
| 64 | + | } | |
| 65 | + | ||
| 66 | + | // Write initial log entry to verify logging works | |
| 67 | + | logger.Info("Logger initialized", "log_path", logPath, "dry_run", dryRun) | |
| 68 | + | ||
| 69 | + | return logger, nil | |
| 31 | 70 | } | |
| 32 | 71 | ||
| 33 | 72 | // NewFromSlog creates a Logger from an existing slog.Logger | |
| 73 | + | // Note: This does not create a log file - the provided slog.Logger should handle output | |
| 34 | 74 | func NewFromSlog(slogLogger *slog.Logger, logPath string, dryRun bool) *Logger { | |
| 35 | 75 | return &Logger{ | |
| 36 | 76 | slog: slogLogger, | |
| 37 | 77 | logPath: logPath, | |
| 78 | + | logFile: nil, // No log file for this constructor | |
| 38 | 79 | dryRun: dryRun, | |
| 39 | 80 | } | |
| 40 | 81 | } | |
| @@ -126,3 +167,11 @@ func (l *Logger) ExecWithConfig(cfg system.RunConfig) system.CommandResult { | |||
| 126 | 167 | func (l *Logger) Slog() *slog.Logger { | |
| 127 | 168 | return l.slog | |
| 128 | 169 | } | |
| 170 | + | ||
| 171 | + | // Close closes the log file (call this when done) | |
| 172 | + | func (l *Logger) Close() error { | |
| 173 | + | if l.logFile != nil { | |
| 174 | + | return l.logFile.Close() | |
| 175 | + | } | |
| 176 | + | return nil | |
| 177 | + | } | |
| @@ -2,6 +2,7 @@ package logger | |||||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | "fmt" | 4 | "fmt" | ||
| 5 | + | "io" | |||
| 5 | "log/slog" | 6 | "log/slog" | ||
| 6 | "os" | 7 | "os" | ||
| 7 | "strings" | 8 | "strings" | ||
| @@ -11,30 +12,70 @@ import ( | |||||
| 11 | 12 | ||||
| 12 | // Logger wraps slog.Logger and adds dry-run mode support | 13 | // Logger wraps slog.Logger and adds dry-run mode support | ||
| 13 | type Logger struct { | 14 | type Logger struct { | ||
| 14 | - | slog *slog.Logger | 15 | + | slog *slog.Logger |
| 15 | - | logPath string | 16 | + | logPath string |
| 16 | - | dryRun bool | 17 | + | dryRun bool |
| 18 | + | logFile *os.File | |||
| 19 | + | } | |||
| 20 | + | ||||
| 21 | + | // syncWriter wraps an io.Writer to sync after every write | |||
| 22 | + | type syncWriter struct { | |||
| 23 | + | w interface { | |||
| 24 | + | io.Writer | |||
| 25 | + | Sync() error | |||
| 26 | + | } | |||
| 27 | + | } | |||
| 28 | + | ||||
| 29 | + | func (sw *syncWriter) Write(p []byte) (n int, err error) { | |||
| 30 | + | n, err = sw.w.Write(p) | |||
| 31 | + | if err != nil { | |||
| 32 | + | return n, err | |||
| 33 | + | } | |||
| 34 | + | // Sync to disk immediately to ensure logs are written even on crash | |||
| 35 | + | if syncErr := sw.w.Sync(); syncErr != nil { | |||
| 36 | + | // Log sync error but don't fail the write | |||
| 37 | + | fmt.Fprintf(os.Stderr, "Warning: failed to sync log file: %v\n", syncErr) | |||
| 38 | + | } | |||
| 39 | + | return n, err | |||
| 17 | } | 40 | } | ||
| 18 | 41 | ||||
| 19 | // New creates a new Logger with optional file logging and dry-run mode | 42 | // New creates a new Logger with optional file logging and dry-run mode | ||
| 20 | func New(logPath string, dryRun bool) (*Logger, error) { | 43 | func New(logPath string, dryRun bool) (*Logger, error) { | ||
| 21 | - | // Create slog handler | 44 | + | // Open log file for writing with sync flag for immediate writes |
| 22 | - | handler := slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{ | 45 | + | logFile, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND|os.O_SYNC, 0644) |
| 46 | + | if err != nil { | |||
| 47 | + | return nil, fmt.Errorf("failed to open log file: %w", err) | |||
| 48 | + | } | |||
| 49 | + | ||||
| 50 | + | // Wrap log file with sync writer to ensure immediate disk writes | |||
| 51 | + | syncLogFile := &syncWriter{w: logFile} | |||
| 52 | + | ||||
| 53 | + | // Only write to log file (not stdout) to avoid interfering with TUI | |||
| 54 | + | // If you need stdout logging for debugging, add it back temporarily | |||
| 55 | + | handler := slog.NewTextHandler(syncLogFile, &slog.HandlerOptions{ | |||
| 23 | Level: slog.LevelInfo, | 56 | Level: slog.LevelInfo, | ||
| 24 | }) | 57 | }) | ||
| 25 | 58 | ||||
| 26 | - | return &Logger{ | 59 | + | logger := &Logger{ |
| 27 | slog: slog.New(handler), | 60 | slog: slog.New(handler), | ||
| 28 | logPath: logPath, | 61 | logPath: logPath, | ||
| 62 | + | logFile: logFile, | |||
| 29 | dryRun: dryRun, | 63 | dryRun: dryRun, | ||
| 30 | - | }, nil | 64 | + | } |
| 65 | + | ||||
| 66 | + | // Write initial log entry to verify logging works | |||
| 67 | + | logger.Info("Logger initialized", "log_path", logPath, "dry_run", dryRun) | |||
| 68 | + | ||||
| 69 | + | return logger, nil | |||
| 31 | } | 70 | } | ||
| 32 | 71 | ||||
| 33 | // NewFromSlog creates a Logger from an existing slog.Logger | 72 | // NewFromSlog creates a Logger from an existing slog.Logger | ||
| 73 | + | // Note: This does not create a log file - the provided slog.Logger should handle output | |||
| 34 | func NewFromSlog(slogLogger *slog.Logger, logPath string, dryRun bool) *Logger { | 74 | func NewFromSlog(slogLogger *slog.Logger, logPath string, dryRun bool) *Logger { | ||
| 35 | return &Logger{ | 75 | return &Logger{ | ||
| 36 | slog: slogLogger, | 76 | slog: slogLogger, | ||
| 37 | logPath: logPath, | 77 | logPath: logPath, | ||
| 78 | + | logFile: nil, // No log file for this constructor | |||
| 38 | dryRun: dryRun, | 79 | dryRun: dryRun, | ||
| 39 | } | 80 | } | ||
| 40 | } | 81 | } | ||
| @@ -126,3 +167,11 @@ func (l *Logger) ExecWithConfig(cfg system.RunConfig) system.CommandResult { | |||||
| 126 | func (l *Logger) Slog() *slog.Logger { | 167 | func (l *Logger) Slog() *slog.Logger { | ||
| 127 | return l.slog | 168 | return l.slog | ||
| 128 | } | 169 | } | ||
| 170 | + | ||||
| 171 | + | // Close closes the log file (call this when done) | |||
| 172 | + | func (l *Logger) Close() error { | |||
| 173 | + | if l.logFile != nil { | |||
| 174 | + | return l.logFile.Close() | |||
| 175 | + | } | |||
| 176 | + | return nil | |||
| 177 | + | } | |||
M internal/ui/model.go +47 -2
| @@ -4,6 +4,7 @@ import ( | |||
| 4 | 4 | "fmt" | |
| 5 | 5 | ||
| 6 | 6 | "github.com/bnema/archup/internal/config" | |
| 7 | + | "github.com/bnema/archup/internal/logger" | |
| 7 | 8 | "github.com/bnema/archup/internal/phases" | |
| 8 | 9 | "github.com/bnema/archup/internal/ui/components" | |
| 9 | 10 | "github.com/bnema/archup/internal/ui/styles" | |
| @@ -33,6 +34,7 @@ type Model struct { | |||
| 33 | 34 | config *config.Config | |
| 34 | 35 | currentForm *huh.Form | |
| 35 | 36 | output *components.OutputViewer | |
| 37 | + | logger *logger.Logger | |
| 36 | 38 | version string | |
| 37 | 39 | width int | |
| 38 | 40 | height int | |
| @@ -40,11 +42,13 @@ type Model struct { | |||
| 40 | 42 | } | |
| 41 | 43 | ||
| 42 | 44 | // NewModel creates a new installer model | |
| 43 | - | func NewModel(orchestrator *phases.Orchestrator, cfg *config.Config, version string) *Model { | |
| 45 | + | func NewModel(orchestrator *phases.Orchestrator, cfg *config.Config, log *logger.Logger, version string) *Model { | |
| 46 | + | log.Info("UI Model created", "version", version) | |
| 44 | 47 | return &Model{ | |
| 45 | 48 | state: StateShowLogo, | |
| 46 | 49 | orchestrator: orchestrator, | |
| 47 | 50 | config: cfg, | |
| 51 | + | logger: log, | |
| 48 | 52 | version: version, | |
| 49 | 53 | width: 80, | |
| 50 | 54 | height: 24, | |
| @@ -66,23 +70,51 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { | |||
| 66 | 70 | m.output.SetSize(msg.Width, msg.Height-10) | |
| 67 | 71 | ||
| 68 | 72 | case tea.KeyMsg: | |
| 73 | + | m.logger.Info("Key pressed", "key", msg.String(), "state", m.state) | |
| 74 | + | ||
| 69 | 75 | switch msg.String() { | |
| 70 | 76 | case "ctrl+c": | |
| 77 | + | m.logger.Info("User cancelled installation") | |
| 71 | 78 | return m, tea.Quit | |
| 72 | 79 | ||
| 73 | 80 | case "enter": | |
| 74 | 81 | switch m.state { | |
| 75 | 82 | case StateShowLogo: | |
| 76 | - | // Move to preflight form | |
| 83 | + | m.logger.Info("Moving to preflight form") | |
| 84 | + | m.state = StatePreflightForm | |
| 85 | + | m.currentForm = CreatePreflightForm(m.config) | |
| 86 | + | return m, m.currentForm.Init() | |
| 87 | + | ||
| 88 | + | case StateConfirmation: | |
| 89 | + | m.logger.Info("User confirmed installation - starting execution") | |
| 90 | + | m.state = StateExecuting | |
| 91 | + | return m, m.executeNextPhase() | |
| 92 | + | } | |
| 93 | + | ||
| 94 | + | case "backspace", "esc": | |
| 95 | + | m.logger.Info("User navigating back", "from_state", m.state) | |
| 96 | + | switch m.state { | |
| 97 | + | case StateDiskForm: | |
| 77 | 98 | m.state = StatePreflightForm | |
| 78 | 99 | m.currentForm = CreatePreflightForm(m.config) | |
| 79 | 100 | return m, m.currentForm.Init() | |
| 101 | + | ||
| 102 | + | case StateOptionsForm: | |
| 103 | + | m.state = StateDiskForm | |
| 104 | + | m.currentForm = CreateDiskSelectionForm(m.config) | |
| 105 | + | return m, m.currentForm.Init() | |
| 106 | + | ||
| 107 | + | case StateConfirmation: | |
| 108 | + | m.state = StateOptionsForm | |
| 109 | + | m.currentForm = CreateOptionsForm(m.config) | |
| 110 | + | return m, m.currentForm.Init() | |
| 80 | 111 | } | |
| 81 | 112 | } | |
| 82 | 113 | ||
| 83 | 114 | case phaseCompleteMsg: | |
| 84 | 115 | switch { | |
| 85 | 116 | case msg.err != nil: | |
| 117 | + | m.logger.Error("Phase execution failed", "error", msg.err) | |
| 86 | 118 | m.err = msg.err | |
| 87 | 119 | m.state = StateError | |
| 88 | 120 | return m, tea.Quit | |
| @@ -91,9 +123,11 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { | |||
| 91 | 123 | // Check if all phases complete | |
| 92 | 124 | switch { | |
| 93 | 125 | case m.orchestrator.IsComplete(): | |
| 126 | + | m.logger.Info("All phases completed successfully") | |
| 94 | 127 | m.state = StateComplete | |
| 95 | 128 | return m, tea.Quit | |
| 96 | 129 | default: | |
| 130 | + | m.logger.Info("Continuing to next phase") | |
| 97 | 131 | // Continue with next phase | |
| 98 | 132 | return m, m.executeNextPhase() | |
| 99 | 133 | } | |
| @@ -179,8 +213,15 @@ func (m *Model) View() string { | |||
| 179 | 213 | ||
| 180 | 214 | // handleFormComplete handles form completion and moves to next state | |
| 181 | 215 | func (m *Model) handleFormComplete() tea.Cmd { | |
| 216 | + | m.logger.Info("Form completed", "state", m.state) | |
| 217 | + | ||
| 182 | 218 | switch m.state { | |
| 183 | 219 | case StatePreflightForm: | |
| 220 | + | m.logger.Info("Preflight form completed", | |
| 221 | + | "hostname", m.config.Hostname, | |
| 222 | + | "username", m.config.Username, | |
| 223 | + | "use_same_password_for_encryption", m.config.UseSamePasswordForEncryption) | |
| 224 | + | ||
| 184 | 225 | // Handle encryption password setting based on checkbox | |
| 185 | 226 | switch { | |
| 186 | 227 | case m.config.UseSamePasswordForEncryption: | |
| @@ -192,11 +233,15 @@ func (m *Model) handleFormComplete() tea.Cmd { | |||
| 192 | 233 | return m.currentForm.Init() | |
| 193 | 234 | ||
| 194 | 235 | case StateDiskForm: | |
| 236 | + | m.logger.Info("Disk selection completed", "disk", m.config.TargetDisk) | |
| 195 | 237 | m.state = StateOptionsForm | |
| 196 | 238 | m.currentForm = CreateOptionsForm(m.config) | |
| 197 | 239 | return m.currentForm.Init() | |
| 198 | 240 | ||
| 199 | 241 | case StateOptionsForm: | |
| 242 | + | m.logger.Info("Options form completed", | |
| 243 | + | "kernel", m.config.KernelChoice, | |
| 244 | + | "encryption", m.config.EncryptionType) | |
| 200 | 245 | // Move directly to confirmation | |
| 201 | 246 | // Encryption password is already handled in preflight form | |
| 202 | 247 | m.state = StateConfirmation | |
| @@ -4,6 +4,7 @@ import ( | |||||
| 4 | "fmt" | 4 | "fmt" | ||
| 5 | 5 | ||||
| 6 | "github.com/bnema/archup/internal/config" | 6 | "github.com/bnema/archup/internal/config" | ||
| 7 | + | "github.com/bnema/archup/internal/logger" | |||
| 7 | "github.com/bnema/archup/internal/phases" | 8 | "github.com/bnema/archup/internal/phases" | ||
| 8 | "github.com/bnema/archup/internal/ui/components" | 9 | "github.com/bnema/archup/internal/ui/components" | ||
| 9 | "github.com/bnema/archup/internal/ui/styles" | 10 | "github.com/bnema/archup/internal/ui/styles" | ||
| @@ -33,6 +34,7 @@ type Model struct { | |||||
| 33 | config *config.Config | 34 | config *config.Config | ||
| 34 | currentForm *huh.Form | 35 | currentForm *huh.Form | ||
| 35 | output *components.OutputViewer | 36 | output *components.OutputViewer | ||
| 37 | + | logger *logger.Logger | |||
| 36 | version string | 38 | version string | ||
| 37 | width int | 39 | width int | ||
| 38 | height int | 40 | height int | ||
| @@ -40,11 +42,13 @@ type Model struct { | |||||
| 40 | } | 42 | } | ||
| 41 | 43 | ||||
| 42 | // NewModel creates a new installer model | 44 | // NewModel creates a new installer model | ||
| 43 | - | func NewModel(orchestrator *phases.Orchestrator, cfg *config.Config, version string) *Model { | 45 | + | func NewModel(orchestrator *phases.Orchestrator, cfg *config.Config, log *logger.Logger, version string) *Model { |
| 46 | + | log.Info("UI Model created", "version", version) | |||
| 44 | return &Model{ | 47 | return &Model{ | ||
| 45 | state: StateShowLogo, | 48 | state: StateShowLogo, | ||
| 46 | orchestrator: orchestrator, | 49 | orchestrator: orchestrator, | ||
| 47 | config: cfg, | 50 | config: cfg, | ||
| 51 | + | logger: log, | |||
| 48 | version: version, | 52 | version: version, | ||
| 49 | width: 80, | 53 | width: 80, | ||
| 50 | height: 24, | 54 | height: 24, | ||
| @@ -66,23 +70,51 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { | |||||
| 66 | m.output.SetSize(msg.Width, msg.Height-10) | 70 | m.output.SetSize(msg.Width, msg.Height-10) | ||
| 67 | 71 | ||||
| 68 | case tea.KeyMsg: | 72 | case tea.KeyMsg: | ||
| 73 | + | m.logger.Info("Key pressed", "key", msg.String(), "state", m.state) | |||
| 74 | + | ||||
| 69 | switch msg.String() { | 75 | switch msg.String() { | ||
| 70 | case "ctrl+c": | 76 | case "ctrl+c": | ||
| 77 | + | m.logger.Info("User cancelled installation") | |||
| 71 | return m, tea.Quit | 78 | return m, tea.Quit | ||
| 72 | 79 | ||||
| 73 | case "enter": | 80 | case "enter": | ||
| 74 | switch m.state { | 81 | switch m.state { | ||
| 75 | case StateShowLogo: | 82 | case StateShowLogo: | ||
| 76 | - | // Move to preflight form | 83 | + | m.logger.Info("Moving to preflight form") |
| 84 | + | m.state = StatePreflightForm | |||
| 85 | + | m.currentForm = CreatePreflightForm(m.config) | |||
| 86 | + | return m, m.currentForm.Init() | |||
| 87 | + | ||||
| 88 | + | case StateConfirmation: | |||
| 89 | + | m.logger.Info("User confirmed installation - starting execution") | |||
| 90 | + | m.state = StateExecuting | |||
| 91 | + | return m, m.executeNextPhase() | |||
| 92 | + | } | |||
| 93 | + | ||||
| 94 | + | case "backspace", "esc": | |||
| 95 | + | m.logger.Info("User navigating back", "from_state", m.state) | |||
| 96 | + | switch m.state { | |||
| 97 | + | case StateDiskForm: | |||
| 77 | m.state = StatePreflightForm | 98 | m.state = StatePreflightForm | ||
| 78 | m.currentForm = CreatePreflightForm(m.config) | 99 | m.currentForm = CreatePreflightForm(m.config) | ||
| 79 | return m, m.currentForm.Init() | 100 | return m, m.currentForm.Init() | ||
| 101 | + | ||||
| 102 | + | case StateOptionsForm: | |||
| 103 | + | m.state = StateDiskForm | |||
| 104 | + | m.currentForm = CreateDiskSelectionForm(m.config) | |||
| 105 | + | return m, m.currentForm.Init() | |||
| 106 | + | ||||
| 107 | + | case StateConfirmation: | |||
| 108 | + | m.state = StateOptionsForm | |||
| 109 | + | m.currentForm = CreateOptionsForm(m.config) | |||
| 110 | + | return m, m.currentForm.Init() | |||
| 80 | } | 111 | } | ||
| 81 | } | 112 | } | ||
| 82 | 113 | ||||
| 83 | case phaseCompleteMsg: | 114 | case phaseCompleteMsg: | ||
| 84 | switch { | 115 | switch { | ||
| 85 | case msg.err != nil: | 116 | case msg.err != nil: | ||
| 117 | + | m.logger.Error("Phase execution failed", "error", msg.err) | |||
| 86 | m.err = msg.err | 118 | m.err = msg.err | ||
| 87 | m.state = StateError | 119 | m.state = StateError | ||
| 88 | return m, tea.Quit | 120 | return m, tea.Quit | ||
| @@ -91,9 +123,11 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { | |||||
| 91 | // Check if all phases complete | 123 | // Check if all phases complete | ||
| 92 | switch { | 124 | switch { | ||
| 93 | case m.orchestrator.IsComplete(): | 125 | case m.orchestrator.IsComplete(): | ||
| 126 | + | m.logger.Info("All phases completed successfully") | |||
| 94 | m.state = StateComplete | 127 | m.state = StateComplete | ||
| 95 | return m, tea.Quit | 128 | return m, tea.Quit | ||
| 96 | default: | 129 | default: | ||
| 130 | + | m.logger.Info("Continuing to next phase") | |||
| 97 | // Continue with next phase | 131 | // Continue with next phase | ||
| 98 | return m, m.executeNextPhase() | 132 | return m, m.executeNextPhase() | ||
| 99 | } | 133 | } | ||
| @@ -179,8 +213,15 @@ func (m *Model) View() string { | |||||
| 179 | 213 | ||||
| 180 | // handleFormComplete handles form completion and moves to next state | 214 | // handleFormComplete handles form completion and moves to next state | ||
| 181 | func (m *Model) handleFormComplete() tea.Cmd { | 215 | func (m *Model) handleFormComplete() tea.Cmd { | ||
| 216 | + | m.logger.Info("Form completed", "state", m.state) | |||
| 217 | + | ||||
| 182 | switch m.state { | 218 | switch m.state { | ||
| 183 | case StatePreflightForm: | 219 | case StatePreflightForm: | ||
| 220 | + | m.logger.Info("Preflight form completed", | |||
| 221 | + | "hostname", m.config.Hostname, | |||
| 222 | + | "username", m.config.Username, | |||
| 223 | + | "use_same_password_for_encryption", m.config.UseSamePasswordForEncryption) | |||
| 224 | + | ||||
| 184 | // Handle encryption password setting based on checkbox | 225 | // Handle encryption password setting based on checkbox | ||
| 185 | switch { | 226 | switch { | ||
| 186 | case m.config.UseSamePasswordForEncryption: | 227 | case m.config.UseSamePasswordForEncryption: | ||
| @@ -192,11 +233,15 @@ func (m *Model) handleFormComplete() tea.Cmd { | |||||
| 192 | return m.currentForm.Init() | 233 | return m.currentForm.Init() | ||
| 193 | 234 | ||||
| 194 | case StateDiskForm: | 235 | case StateDiskForm: | ||
| 236 | + | m.logger.Info("Disk selection completed", "disk", m.config.TargetDisk) | |||
| 195 | m.state = StateOptionsForm | 237 | m.state = StateOptionsForm | ||
| 196 | m.currentForm = CreateOptionsForm(m.config) | 238 | m.currentForm = CreateOptionsForm(m.config) | ||
| 197 | return m.currentForm.Init() | 239 | return m.currentForm.Init() | ||
| 198 | 240 | ||||
| 199 | case StateOptionsForm: | 241 | case StateOptionsForm: | ||
| 242 | + | m.logger.Info("Options form completed", | |||
| 243 | + | "kernel", m.config.KernelChoice, | |||
| 244 | + | "encryption", m.config.EncryptionType) | |||
| 200 | // Move directly to confirmation | 245 | // Move directly to confirmation | ||
| 201 | // Encryption password is already handled in preflight form | 246 | // Encryption password is already handled in preflight form | ||
| 202 | m.state = StateConfirmation | 247 | m.state = StateConfirmation | ||