refactor for separation of concern for better readabiliy and maintainability

This commit is contained in:
Robin Olsen
2026-03-03 11:41:07 +01:00
parent e8d9f9d1b6
commit 763f079063
11 changed files with 530 additions and 471 deletions

View File

@@ -4,7 +4,6 @@ import (
"context"
"fmt"
"log/slog"
"time"
"github.com/LazyBachelor/LazyPM/internal/models"
tea "github.com/charmbracelet/bubbletea"
@@ -12,12 +11,9 @@ import (
type App = models.App
type Config = models.Config
type Tasker = models.Tasker
type Interface = models.Interface
type InterfaceType = models.InterfaceType
type ValidatedInterface = models.ValidatedInterface
type ValidationFeedback = models.ValidationFeedback
@@ -27,179 +23,174 @@ type QuestionnaireKeysProvider interface {
var ErrUserQuit = models.ErrUserQuit
// RunTask orchestrates the complete task execution flow:
// 1. Setup the task
// 2. Show task intro screen
// 3. Run the interface
// 4. Start validation loop in background
// 5. Show questionnaire when done
func RunTask(ctx context.Context, app *App, t Tasker, i Interface, iType InterfaceType) (runErr error) {
config := t.Config()
details := t.Details()
type TaskRunner struct {
app *App
logger *slog.Logger
}
func NewTaskRunner(app *App) *TaskRunner {
var logger *slog.Logger
if app != nil {
logger = app.Logger
}
return &TaskRunner{
app: app,
logger: logger,
}
}
collector := newTaskRunCollector(details.Title, iType, logger)
collector.log("info", "task run started")
func (r *TaskRunner) Run(ctx context.Context, t Tasker, i Interface, iType InterfaceType) (runErr error) {
config = config.WithActionLogger(func(action string) {
collector.recordUserAction(action)
})
config := t.Config()
details := t.Details()
lifecycle := NewRunLifecycle(r.app, config, details, iType, r.logger)
defer func() {
if runErr != nil {
collector.setError(runErr)
collector.log("error", runErr.Error())
}
run := collector.finalize()
if err := appendTaskMetrics(config.StatisticsStoragePath, details.Title, run, logger); err != nil {
if runErr == nil {
runErr = fmt.Errorf("failed to persist task metrics: %w", err)
return
}
if logger != nil {
logger.Warn("failed to persist task metrics", "error", err, "task", details.Title)
}
}
if app != nil && app.Stats != nil {
if err := app.Stats.RecordTaskRun(ctx, run); err != nil {
if runErr == nil {
runErr = fmt.Errorf("failed to update global statistics: %w", err)
return
}
if logger != nil {
logger.Warn("failed to update global statistics", "error", err, "task", details.Title)
}
}
}
runErr = lifecycle.Finish(ctx, runErr)
}()
doneChan := make(chan bool, 1)
quitChan := make(chan bool, 1)
collector := lifecycle.collector
config = lifecycle.config
collector.log("info", "task run started")
// Setup
if err := t.Setup(ctx); err != nil {
return fmt.Errorf("failed to setup task: %w", err)
}
// Intro screen
if err := runIntro(details); err != nil {
return err
}
// Validation
feedbackChan := make(chan ValidationFeedback, 10)
quitChan := make(chan bool, 1)
if validated, ok := i.(ValidatedInterface); ok {
validated.SetChannels(feedbackChan, quitChan)
}
// Setup task
collector.log("info", "setting up task")
if err := t.Setup(ctx); err != nil {
return fmt.Errorf("failed to setup task: %w", err)
}
collector.log("info", "task setup completed")
engine := &ValidationEngine{task: t}
doneChan, stopChan := engine.Start(ctx, func(feedback ValidationFeedback) {
collector.recordValidation(feedback)
// Show task intro
collector.log("info", "showing task intro")
detailsScreen := NewTaskModel(details)
model, err := tea.NewProgram(detailsScreen, tea.WithAltScreen()).Run()
if err != nil {
return err
}
if m, ok := model.(interface{ GetUserQuit() bool }); ok && m.GetUserQuit() {
collector.log("info", "user quit during task intro")
return ErrUserQuit
}
collector.log("info", "task intro completed")
if feedback.Success {
feedback.Message = "Task completed successfully!"
} else if feedback.Message == "" {
feedback.Message = "Task not completed!"
}
// Start validation loop
collector.log("info", "starting validation loop")
go startValidationLoop(ctx, t, feedbackChan, doneChan, quitChan, collector.recordValidation)
select {
case feedbackChan <- feedback:
default:
}
})
// Run interface
collector.log("info", "starting task interface")
interfaceDone := make(chan error, 1)
interfaceErr := make(chan error, 1)
go func() {
interfaceDone <- i.Run(ctx, config)
interfaceErr <- i.Run(ctx, config)
}()
select {
case <-doneChan:
close(stopChan)
close(quitChan)
collector.setCompleted(true)
collector.log("info", "task validation completed")
if err := <-interfaceDone; err != nil {
if logger != nil {
logger.Warn("interface error after task completion", "error", err, "task", details.Title)
}
collector.log("warn", fmt.Sprintf("interface error after task completion: %v", err))
}
fmt.Println("Task completed successfully!")
case err := <-interfaceDone:
case err := <-interfaceErr:
close(stopChan)
close(quitChan)
if err != nil {
collector.log("error", fmt.Sprintf("task interface failed: %v", err))
return fmt.Errorf("failed to start task interface: %w", err)
return fmt.Errorf("task interface failed: %w", err)
}
collector.log("info", "task interface exited before completion")
fmt.Println("Task incomplete - you exited early")
}
// Show questionnaire
collector.log("info", "showing post-task questionnaire")
questions := t.Questions(iType)
questionnaireKeys := []string{}
if provider, ok := t.(QuestionnaireKeysProvider); ok {
questionnaireKeys = provider.QuestionnaireKeys(iType)
// Questionnaire
if err := runQuestionnaire(t, iType, collector); err != nil {
return err
}
questionare := NewQuestionnaireModel(questions, questionnaireKeys)
model, err = tea.NewProgram(questionare, tea.WithAltScreen()).Run()
collector.log("info", "task run finished")
return nil
}
func (r *RunLifecycle) Finish(ctx context.Context, runErr error) error {
if runErr != nil {
r.collector.setError(runErr)
r.collector.log("error", runErr.Error())
}
run := r.collector.finalize()
if r.metricsStore != nil {
if err := r.metricsStore.Append(ctx, r.details.Title, run); err != nil && runErr == nil {
return fmt.Errorf("persist metrics: %w", err)
}
}
if r.app != nil && r.app.Stats != nil {
if err := r.app.Stats.RecordTaskRun(ctx, run); err != nil && runErr == nil {
return fmt.Errorf("update global stats: %w", err)
}
}
return runErr
}
func runIntro(details models.TaskDetails) error {
model, err := tea.NewProgram(NewTaskModel(details), tea.WithAltScreen()).Run()
if err != nil {
return err
}
questionnaireCompleted := false
questionnaireAnswers := map[string]any(nil)
if m, ok := model.(interface {
GetCompleted() bool
GetAnswers() map[string]any
}); ok {
questionnaireCompleted = m.GetCompleted()
questionnaireAnswers = m.GetAnswers()
if m, ok := model.(interface{ GetUserQuit() bool }); ok {
if m.GetUserQuit() {
return ErrUserQuit
}
}
if m, ok := model.(interface{ GetUserQuit() bool }); ok && m.GetUserQuit() {
collector.recordQuestionnaire(questionnaireCompleted, true, questionnaireAnswers)
collector.log("info", "user quit during questionnaire")
return ErrUserQuit
}
collector.recordQuestionnaire(questionnaireCompleted, false, questionnaireAnswers)
collector.log("info", "task run finished")
return nil
}
func startValidationLoop(ctx context.Context, t Tasker, feedbackChan chan ValidationFeedback, doneChan chan bool, quitChan chan bool, onFeedback func(ValidationFeedback)) {
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
func runQuestionnaire(t Tasker, iType InterfaceType, collector *taskRunCollector) error {
questions := t.Questions(iType)
for {
select {
case <-ticker.C:
feedback := t.Validate(ctx)
if onFeedback != nil {
onFeedback(feedback)
}
if feedback.Success {
feedback.Message = "Task completed successfully!"
feedbackChan <- feedback
time.Sleep(4 * time.Second)
doneChan <- true
return
}
feedback.Message = "Task not completed!"
feedbackChan <- feedback
case <-quitChan:
return
case <-ctx.Done():
return
}
keys := []string{}
if provider, ok := t.(QuestionnaireKeysProvider); ok {
keys = provider.QuestionnaireKeys(iType)
}
model, err := tea.NewProgram(NewQuestionnaireModel(questions, keys), tea.WithAltScreen()).Run()
if err != nil {
return err
}
var completed bool
var answers map[string]any
if m, ok := model.(interface {
GetCompleted() bool
GetAnswers() map[string]any
}); ok {
completed = m.GetCompleted()
answers = m.GetAnswers()
}
userQuit := false
if m, ok := model.(interface{ GetUserQuit() bool }); ok {
userQuit = m.GetUserQuit()
}
collector.recordQuestionnaire(completed, userQuit, answers)
if userQuit {
return ErrUserQuit
}
return nil
}