From 63631fc42f64038ca346dad144ac2743a16c2be0 Mon Sep 17 00:00:00 2001 From: Moira Daniella A Sebastian Date: Mon, 2 Mar 2026 20:17:08 +0100 Subject: [PATCH 01/51] Implemented add comments for tui --- pkg/tui/views/dashboard/help_bar.go | 3 +- pkg/tui/views/dashboard/issue_detail.go | 42 +++++++++++--- pkg/tui/views/dashboard/keys.go | 24 +++++--- pkg/tui/views/dashboard/model.go | 35 ++++++++++-- pkg/tui/views/dashboard/operations.go | 74 ++++++++++++++++++++++--- pkg/tui/views/dashboard/view.go | 15 +++++ 6 files changed, 165 insertions(+), 28 deletions(-) diff --git a/pkg/tui/views/dashboard/help_bar.go b/pkg/tui/views/dashboard/help_bar.go index 3ad63cc..d5b7891 100644 --- a/pkg/tui/views/dashboard/help_bar.go +++ b/pkg/tui/views/dashboard/help_bar.go @@ -36,7 +36,7 @@ func (h HelpBar) shortHelp() string { styles.HighlightKey("↓/j") + " down", styles.HighlightKey("pgup/pgdn") + " page", styles.HighlightKey("a") + " add", - styles.HighlightKey("e/d/s/p/t") + " edit", + styles.HighlightKey("e/d/s/p/t/c") + " edit", styles.HighlightKey("x") + " delete", styles.HighlightKey("q") + " quit", styles.HighlightKey("?") + " help", @@ -75,6 +75,7 @@ func (h HelpBar) fullHelp() string { renderRow("pgup", "page up", "pgdn", "page down"), renderRow("b", "back to list", "a", "add issue"), renderRow("e", "edit title", "d", "edit desc"), + renderRow("c", "add comment", "", ""), renderRow("s", "change status", "p", "change priority"), renderRow("t", "change type", "x", "delete issue"), renderRow("?", "help", "q", "quit"), diff --git a/pkg/tui/views/dashboard/issue_detail.go b/pkg/tui/views/dashboard/issue_detail.go index 8681800..2899bca 100644 --- a/pkg/tui/views/dashboard/issue_detail.go +++ b/pkg/tui/views/dashboard/issue_detail.go @@ -1,6 +1,8 @@ package dashboard import ( + "time" + "github.com/LazyBachelor/LazyPM/internal/models" "github.com/LazyBachelor/LazyPM/pkg/tui/styles" "github.com/charmbracelet/bubbles/viewport" @@ -10,6 +12,7 @@ import ( type IssueDetail struct { viewport viewport.Model issue models.Issue + comments []*models.Comment focused bool } @@ -25,6 +28,12 @@ func (i *IssueDetail) SetIssue(issue models.Issue) { i.refreshContent() } +// SetComments updates the list of comments displayed for the current issue. +func (i *IssueDetail) SetComments(comments []*models.Comment) { + i.comments = comments + i.refreshContent() +} + func (i *IssueDetail) SetSize(width, height int) { i.viewport.Height = height i.viewport.Width = width @@ -60,19 +69,34 @@ func (i *IssueDetail) refreshContent() { descLabel := styles.LabelStyle.Render("Description:") descContent := styles.ValueStyle.Render(i.issue.Description) - content := lipgloss.JoinVertical(lipgloss.Left, - titleRow, - idRow, - typeRow, - statusRow, - priorityRow, - descLabel, - descContent, - ) + var parts []string + parts = append(parts, titleRow, idRow, typeRow, statusRow, priorityRow, descLabel, descContent) + // Comments section + commentsLabel := styles.LabelStyle.Render("Comments:") + parts = append(parts, commentsLabel) + if len(i.comments) == 0 { + parts = append(parts, lipgloss.NewStyle().Foreground(styles.FaintText).Render(" No comments yet.")) + } else { + for _, c := range i.comments { + authorDate := lipgloss.NewStyle().Foreground(styles.Primary).Render(c.Author) + " " + + lipgloss.NewStyle().Foreground(styles.FaintText).Render(formatCommentTime(c.CreatedAt)) + commentRow := lipgloss.JoinVertical(lipgloss.Left, + authorDate, + styles.ValueStyle.Render(c.Text), + ) + parts = append(parts, commentRow) + } + } + + content := lipgloss.JoinVertical(lipgloss.Left, parts...) i.viewport.SetContent(content) } +func formatCommentTime(t time.Time) string { + return t.Format("Jan 2, 15:04") +} + func (i IssueDetail) View() string { content := i.viewport.View() diff --git a/pkg/tui/views/dashboard/keys.go b/pkg/tui/views/dashboard/keys.go index 34760f5..8f44517 100644 --- a/pkg/tui/views/dashboard/keys.go +++ b/pkg/tui/views/dashboard/keys.go @@ -18,6 +18,7 @@ type DashboardKeyMap struct { ChangeStatus key.Binding ChangePriority key.Binding ChangeType key.Binding + AddComment key.Binding AddIssue key.Binding DeleteIssue key.Binding } @@ -71,6 +72,10 @@ var defaultDashboardKeyMap = DashboardKeyMap{ key.WithKeys("t"), key.WithHelp("t", "change type"), ), + AddComment: key.NewBinding( + key.WithKeys("c"), + key.WithHelp("c", "add comment"), + ), AddIssue: key.NewBinding( key.WithKeys("a"), key.WithHelp("a", "add issue"), @@ -99,32 +104,37 @@ func (d *Model) handleKeyMsg(msg tea.KeyMsg) tea.Cmd { d.issueDetail.ScrollUp(1) case d.IsFocusedOnDetail() && key.Matches(msg, d.keyMap.ScrollDown): d.issueDetail.ScrollDown(1) - case !d.editingTitle && !d.creatingIssue && !d.editingDescription && !d.choosingStatus && !d.choosingPriority && !d.confirmingDelete && !d.choosingType && key.Matches(msg, d.keyMap.EditTitle): + case !d.editingTitle && !d.creatingIssue && !d.editingDescription && !d.choosingStatus && !d.choosingPriority && !d.confirmingDelete && !d.choosingType && !d.addingComment && key.Matches(msg, d.keyMap.EditTitle): if selected := d.FocusedIssueList().SelectedItem(); selected.ID != "" { d.startEditTitle(selected) cmd = d.titleInput.Focus() } - case !d.editingTitle && !d.creatingIssue && !d.editingDescription && !d.choosingStatus && !d.choosingPriority && !d.confirmingDelete && !d.choosingType && key.Matches(msg, d.keyMap.EditDescription): + case !d.editingTitle && !d.creatingIssue && !d.editingDescription && !d.choosingStatus && !d.choosingPriority && !d.confirmingDelete && !d.choosingType && !d.addingComment && key.Matches(msg, d.keyMap.EditDescription): if selected := d.FocusedIssueList().SelectedItem(); selected.ID != "" { d.startEditDescription(selected) cmd = d.descriptionInput.Focus() } - case !d.editingTitle && !d.creatingIssue && !d.editingDescription && !d.choosingStatus && !d.choosingPriority && !d.confirmingDelete && !d.choosingType && key.Matches(msg, d.keyMap.ChangeStatus): + case !d.editingTitle && !d.creatingIssue && !d.editingDescription && !d.choosingStatus && !d.choosingPriority && !d.confirmingDelete && !d.choosingType && !d.addingComment && key.Matches(msg, d.keyMap.ChangeStatus): if selected := d.FocusedIssueList().SelectedItem(); selected.ID != "" { d.startChooseStatus(selected) } - case !d.editingTitle && !d.creatingIssue && !d.editingDescription && !d.choosingStatus && !d.choosingPriority && !d.confirmingDelete && !d.choosingType && key.Matches(msg, d.keyMap.ChangePriority): + case !d.editingTitle && !d.creatingIssue && !d.editingDescription && !d.choosingStatus && !d.choosingPriority && !d.confirmingDelete && !d.choosingType && !d.addingComment && key.Matches(msg, d.keyMap.ChangePriority): if selected := d.FocusedIssueList().SelectedItem(); selected.ID != "" { d.startChoosePriority(selected) } - case !d.editingTitle && !d.creatingIssue && !d.editingDescription && !d.choosingStatus && !d.choosingPriority && !d.confirmingDelete && !d.choosingType && key.Matches(msg, d.keyMap.ChangeType): + case !d.editingTitle && !d.creatingIssue && !d.editingDescription && !d.choosingStatus && !d.choosingPriority && !d.confirmingDelete && !d.choosingType && !d.addingComment && key.Matches(msg, d.keyMap.ChangeType): if selected := d.FocusedIssueList().SelectedItem(); selected.ID != "" { d.startChooseType(selected) } - case !d.editingTitle && !d.creatingIssue && !d.editingDescription && !d.choosingStatus && !d.choosingPriority && !d.confirmingDelete && !d.choosingType && key.Matches(msg, d.keyMap.AddIssue): + case !d.editingTitle && !d.creatingIssue && !d.editingDescription && !d.choosingStatus && !d.choosingPriority && !d.confirmingDelete && !d.choosingType && !d.addingComment && key.Matches(msg, d.keyMap.AddComment): + if selected := d.FocusedIssueList().SelectedItem(); selected.ID != "" { + d.startAddComment(selected) + cmd = d.commentInput.Focus() + } + case !d.editingTitle && !d.creatingIssue && !d.editingDescription && !d.choosingStatus && !d.choosingPriority && !d.confirmingDelete && !d.choosingType && !d.addingComment && key.Matches(msg, d.keyMap.AddIssue): d.startCreateIssue() cmd = d.createTitleInput.Focus() - case !d.editingTitle && !d.creatingIssue && !d.editingDescription && !d.choosingStatus && !d.choosingPriority && !d.confirmingDelete && !d.choosingType && key.Matches(msg, d.keyMap.DeleteIssue): + case !d.editingTitle && !d.creatingIssue && !d.editingDescription && !d.choosingStatus && !d.choosingPriority && !d.confirmingDelete && !d.choosingType && !d.addingComment && key.Matches(msg, d.keyMap.DeleteIssue): fl := d.FocusedIssueList() if selected := fl.SelectedItem(); selected.ID != "" { d.startConfirmDelete(selected.ID, fl.Index()) diff --git a/pkg/tui/views/dashboard/model.go b/pkg/tui/views/dashboard/model.go index dde0979..11283fc 100644 --- a/pkg/tui/views/dashboard/model.go +++ b/pkg/tui/views/dashboard/model.go @@ -47,6 +47,9 @@ type Model struct { priorityIssueID string choosingType bool // true while choosing a type typeIssueID string + addingComment bool // true while adding a comment + commentInput textarea.Model + commentIssueID string feedbackChan chan models.ValidationFeedback quitChan chan bool currentFeedback models.ValidationFeedback @@ -89,15 +92,39 @@ func NewDashboard(app *app.App, feedbackChan chan models.ValidationFeedback, qui descTa.SetHeight(8) m.descriptionInput = descTa + commentTa := textarea.New() + commentTa.Placeholder = "Write your comment..." + commentTa.SetWidth(56) + commentTa.SetHeight(6) + m.commentInput = commentTa + if selected := m.issueList.SelectedItem(); selected.ID != "" { - m.issueDetail.SetIssue(selected.Issue) + m.setDetailIssueWithComments(selected.Issue) } else if selected := m.closedIssueList.SelectedItem(); selected.ID != "" { - m.issueDetail.SetIssue(selected.Issue) + m.setDetailIssueWithComments(selected.Issue) } return m } +// setDetailIssueWithComments sets the issue in the detail pane and loads its comments. +func (m *Model) setDetailIssueWithComments(issue models.Issue) { + m.issueDetail.SetIssue(issue) + if issue.ID == "" { + m.issueDetail.SetComments(nil) + return + } + comments, _ := m.app.Issues.GetIssueComments(context.Background(), issue.ID) + m.issueDetail.SetComments(comments) +} + +func (m *Model) startAddComment(selected ListIssue) { + m.addingComment = true + m.commentIssueID = selected.ID + m.commentInput.SetValue("") + m.commentInput.Reset() +} + func (m *Model) startEditTitle(selected ListIssue) { m.editingTitle = true m.editingIssueID = selected.ID @@ -203,11 +230,11 @@ func (m *Model) ToggleFocusedWindow() { m.focusedWindow = 1 - m.focusedWindow if m.focusedWindow == 0 { if selected := m.issueList.SelectedItem(); selected.ID != "" { - m.issueDetail.SetIssue(selected.Issue) + m.setDetailIssueWithComments(selected.Issue) } } else { if selected := m.closedIssueList.SelectedItem(); selected.ID != "" { - m.issueDetail.SetIssue(selected.Issue) + m.setDetailIssueWithComments(selected.Issue) } } m.issueDetail.SetFocused(m.IsFocusedOnDetail()) diff --git a/pkg/tui/views/dashboard/operations.go b/pkg/tui/views/dashboard/operations.go index 8d53ee9..07cfc37 100644 --- a/pkg/tui/views/dashboard/operations.go +++ b/pkg/tui/views/dashboard/operations.go @@ -2,6 +2,8 @@ package dashboard import ( "context" + "os" + "os/user" "github.com/LazyBachelor/LazyPM/internal/app" "github.com/LazyBachelor/LazyPM/internal/models" @@ -9,6 +11,19 @@ import ( tea "github.com/charmbracelet/bubbletea" ) +func defaultCommentAuthor() string { + if u, err := user.Current(); err == nil && u.Username != "" { + return u.Username + } + if s := os.Getenv("USER"); s != "" { + return s + } + if s := os.Getenv("USERNAME"); s != "" { + return s + } + return "user" +} + type issueTitleUpdatedMsg struct { IssueID string Err error @@ -49,6 +64,18 @@ type issueDeletedMsg struct { PreviousIndex int } +type issueCommentAddedMsg struct { + IssueID string + Err error +} + +func addIssueCommentCmd(app *app.App, issueID, author, text string) tea.Cmd { + return func() tea.Msg { + _, err := app.Issues.AddIssueComment(context.Background(), issueID, author, text) + return issueCommentAddedMsg{IssueID: issueID, Err: err} + } +} + func updateIssueTitleCmd(app *app.App, issueID, newTitle string) tea.Cmd { return func() tea.Msg { updates := map[string]interface{}{"title": newTitle} @@ -121,7 +148,7 @@ func (m *Model) refreshIssueListsAndSelectIssue(issueID string) tea.Cmd { closedSetCmd := m.closedIssueList.SetIssues(ClosedOnly(issues)) for _, issue := range issues { if issue.ID == issueID { - m.issueDetail.SetIssue(*issue) + m.setDetailIssueWithComments(*issue) break } } @@ -203,8 +230,17 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } } - m.issueDetail.SetIssue(*selectedIssue) + m.setDetailIssueWithComments(*selectedIssue) return m, tea.Sequence(setItemsCmd, closedSetCmd, func() tea.Msg { return selectIssueMsg{IssueID: selectedIssue.ID} }) + case issueCommentAddedMsg: + m.addingComment = false + m.commentIssueID = "" + m.commentInput.Blur() + m.commentInput.Reset() + if msg.Err != nil { + return m, nil + } + return m, m.refreshIssueListsAndSelectIssue(msg.IssueID) case issueDeletedMsg: m.confirmingDelete = false m.deleteConfirmID = "" @@ -221,7 +257,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { closedSetCmd := m.closedIssueList.SetIssues(closedIssues) // If there are no issues at all, clear the detail view and return. if len(openIssues) == 0 && len(closedIssues) == 0 { - m.issueDetail.SetIssue(models.Issue{}) + m.setDetailIssueWithComments(models.Issue{}) return m, tea.Sequence(setItemsCmd, closedSetCmd) } @@ -245,7 +281,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { // Safety: if targetIssues is still empty here, just clear detail and return. if len(targetIssues) == 0 { - m.issueDetail.SetIssue(models.Issue{}) + m.setDetailIssueWithComments(models.Issue{}) return m, tea.Sequence(setItemsCmd, closedSetCmd) } @@ -254,7 +290,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { newIndex = len(targetIssues) - 1 } selectedIssue := targetIssues[newIndex] - m.issueDetail.SetIssue(*selectedIssue) + m.setDetailIssueWithComments(*selectedIssue) return m, tea.Sequence(setItemsCmd, closedSetCmd, func() tea.Msg { return selectIssueMsg{IssueID: selectedIssue.ID} }) @@ -388,6 +424,30 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, cmd } + if m.addingComment { + if msg.String() == "ctrl+s" || msg.String() == "enter" { + text := m.commentInput.Value() + if text != "" { + issueID := m.commentIssueID + m.addingComment = false + m.commentIssueID = "" + m.commentInput.Blur() + m.commentInput.Reset() + return m, addIssueCommentCmd(m.app, issueID, defaultCommentAuthor(), text) + } + } + if msg.String() == "esc" { + m.addingComment = false + m.commentIssueID = "" + m.commentInput.Blur() + m.commentInput.Reset() + return m, nil + } + var cmd tea.Cmd + m.commentInput, cmd = m.commentInput.Update(msg) + return m, cmd + } + if m.editingDescription { if msg.String() == "ctrl+s" { issueID := m.editingDescIssueID @@ -441,7 +501,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { cmd, changed := m.issueList.Update(msg) if changed { if selected := m.issueList.SelectedItem(); selected.ID != "" { - m.issueDetail.SetIssue(selected.Issue) + m.setDetailIssueWithComments(selected.Issue) } } return m, cmd @@ -449,7 +509,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { cmd, changed := m.closedIssueList.Update(msg) if changed { if selected := m.closedIssueList.SelectedItem(); selected.ID != "" { - m.issueDetail.SetIssue(selected.Issue) + m.setDetailIssueWithComments(selected.Issue) } } return m, cmd diff --git a/pkg/tui/views/dashboard/view.go b/pkg/tui/views/dashboard/view.go index 2df8781..74aca51 100644 --- a/pkg/tui/views/dashboard/view.go +++ b/pkg/tui/views/dashboard/view.go @@ -70,6 +70,21 @@ func (m *Model) View() string { return lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, editBox) } + if m.addingComment { + editBoxWidth := min(60, m.width-4) + m.commentInput.SetWidth(editBoxWidth - 2) + m.commentInput.SetHeight(8) + editContent := lipgloss.JoinVertical(lipgloss.Left, + styles.LabelStyle.Render("Add comment for "+m.commentIssueID+" (Ctrl+S or Enter to save, Esc to cancel):"), + m.commentInput.View(), + ) + editBox := styles.ContainerStyle. + Width(editBoxWidth). + BorderForeground(styles.PrimaryBorder). + Render(editContent) + return lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, editBox) + } + if m.editingDescription { editBoxWidth := min(60, m.width-4) m.descriptionInput.SetWidth(editBoxWidth - 2) From a8f8f63e88471a400f112b2cf6731a8d9a55f9fc Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Mon, 2 Mar 2026 23:04:57 +0100 Subject: [PATCH 02/51] configure json logger --- internal/app/options.go | 34 ++++++++++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/internal/app/options.go b/internal/app/options.go index 840c865..d288c28 100644 --- a/internal/app/options.go +++ b/internal/app/options.go @@ -2,8 +2,10 @@ package app import ( "context" + "io" "log/slog" "os" + "path/filepath" "github.com/LazyBachelor/LazyPM/internal/models" ) @@ -23,15 +25,43 @@ type AppBuilder struct { } func defaultBuilder(ctx context.Context, config Config) *AppBuilder { + lifecycle := NewLifecycle() + logger := newDefaultLogger(config, lifecycle) + return &AppBuilder{ ctx: ctx, config: config, - lifecycle: NewLifecycle(), + lifecycle: lifecycle, initializer: &InteractiveInitializer{}, - logger: slog.New(slog.NewJSONHandler(os.Stdout, nil)), + logger: logger, } } +func newDefaultLogger(config Config, lifecycle *Lifecycle) *slog.Logger { + statsDir := filepath.Dir(config.StatisticsStoragePath) + if statsDir == "" { + statsDir = "." + } + + if err := os.MkdirAll(statsDir, 0o755); err != nil { + return slog.New(slog.NewJSONHandler(io.Discard, nil)) + } + + logPath := filepath.Join(statsDir, "app-logs.jsonl") + logFile, err := os.OpenFile(logPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644) + if err != nil { + return slog.New(slog.NewJSONHandler(io.Discard, nil)) + } + + if lifecycle != nil { + lifecycle.Add(func() { + _ = logFile.Close() + }) + } + + return slog.New(slog.NewJSONHandler(logFile, &slog.HandlerOptions{})) +} + func WithLogger(logger *slog.Logger) Option { return func(b *AppBuilder) error { b.logger = logger From 4debb69de75eb8ae393fdb1f1e9db749fbd15ee1 Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Mon, 2 Mar 2026 23:06:34 +0100 Subject: [PATCH 03/51] start with action logger --- internal/app/app.go | 7 ++++--- internal/models/action_event.go | 29 +++++++++++++++++++++++++++++ internal/models/app.go | 14 ++++++++++++++ 3 files changed, 47 insertions(+), 3 deletions(-) create mode 100644 internal/models/action_event.go diff --git a/internal/app/app.go b/internal/app/app.go index bb7a4cb..b44afa2 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -46,7 +46,7 @@ func New(ctx context.Context, config Config, opts ...Option) (*App, func(), erro StartTime: time.Now(), }) - jsonStatService, err := NewStatisticsService(statStore) + jsonStatService, err := NewStatisticsService(statStore, b.logger) if err != nil { return nil, nil, err } @@ -58,8 +58,9 @@ func New(ctx context.Context, config Config, opts ...Option) (*App, func(), erro Config: config, Logger: b.logger, - Issues: b.issueService, - Stats: b.statsService, + Issues: b.issueService, + Stats: b.statsService, + ActionLogger: config.ActionLogger, } return app, b.lifecycle.Close, nil diff --git a/internal/models/action_event.go b/internal/models/action_event.go new file mode 100644 index 0000000..3b43b1d --- /dev/null +++ b/internal/models/action_event.go @@ -0,0 +1,29 @@ +package models + +import "encoding/json" + +type ActionEvent struct { + Source string `json:"source"` + Action string `json:"action"` + Target string `json:"target,omitempty"` + Result string `json:"result,omitempty"` +} + +func EncodeActionEvent(event ActionEvent) string { + bytes, err := json.Marshal(event) + if err != nil { + return event.Action + } + return string(bytes) +} + +func DecodeActionEvent(raw string) (ActionEvent, bool) { + var event ActionEvent + if err := json.Unmarshal([]byte(raw), &event); err != nil { + return ActionEvent{}, false + } + if event.Source == "" && event.Action == "" && event.Target == "" && event.Result == "" { + return ActionEvent{}, false + } + return event, true +} diff --git a/internal/models/app.go b/internal/models/app.go index 4a3b30d..efeb815 100644 --- a/internal/models/app.go +++ b/internal/models/app.go @@ -16,6 +16,19 @@ type App struct { Interfaces map[string]Interface CurrentFeedback *ValidationFeedback + ActionLogger func(string) +} + +func (a *App) LogAction(action string) { + if a == nil || action == "" { + return + } + if a.Logger != nil { + a.Logger.Info("action event", "action", action) + } + if a.ActionLogger != nil { + a.ActionLogger(action) + } } // IssueService defines the interface for managing issues in the application. @@ -42,4 +55,5 @@ type StatsService interface { Load(ctx context.Context) error Save(ctx context.Context) error GetStatistics() (Statistics, error) + RecordTaskRun(ctx context.Context, run TaskRunMetrics) error } From 672644515933ac7de37dcfb9f6d0050ebb0e878a Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Mon, 2 Mar 2026 23:07:25 +0100 Subject: [PATCH 04/51] define stat and metrics model --- internal/models/statistics.go | 79 ++++++++++++++++++++++++++++++++++- 1 file changed, 78 insertions(+), 1 deletion(-) diff --git a/internal/models/statistics.go b/internal/models/statistics.go index 8ad224a..6581084 100644 --- a/internal/models/statistics.go +++ b/internal/models/statistics.go @@ -11,10 +11,87 @@ type Statistics struct { Duration time.Duration `json:"duration"` InterfaceType InterfaceType `json:"interface_type"` + TaskRuns int `json:"task_runs"` TasksCompleted int `json:"tasks_completed"` - ButtonClicks ButtonClicks `json:"button_clicks"` + TasksFailed int `json:"tasks_failed"` + + TotalDurationMs int64 `json:"total_duration_ms"` + AverageDurationMs int64 `json:"average_duration_ms"` + + TotalUserActions int `json:"total_user_actions"` + QuestionnairesCompleted int `json:"questionnaires_completed"` + QuestionnairesAbandoned int `json:"questionnaires_abandoned"` + + ValidationAttempts int `json:"validation_attempts"` + ValidationSuccesses int `json:"validation_successes"` + ValidationFailures int `json:"validation_failures"` + ValidationChecksPassed int `json:"validation_checks_passed"` + ValidationChecksFailed int `json:"validation_checks_failed"` + + LastTaskName string `json:"last_task_name"` + LastRunID int `json:"last_run_id"` + ButtonClicks ButtonClicks `json:"button_clicks"` } type ButtonClicks struct { Clicks int `json:"clicks"` } + +type TaskMetricsFile struct { + TaskName string `json:"task_name"` + UpdatedAt time.Time `json:"updated_at"` + Summary TaskStatsSummary `json:"summary"` + Runs []TaskRunMetrics `json:"runs"` +} + +type TaskStatsSummary struct { + TotalRuns int `json:"total_runs"` + CompletedRuns int `json:"completed_runs"` + IncompleteRuns int `json:"incomplete_runs"` + TotalDurationMs int64 `json:"total_duration_ms"` + AverageDurationMs int64 `json:"average_duration_ms"` + TotalUserActions int `json:"total_user_actions"` + QuestionnairesCompleted int `json:"questionnaires_completed"` + QuestionnairesAbandoned int `json:"questionnaires_abandoned"` + ValidationAttempts int `json:"validation_attempts"` + ValidationSuccesses int `json:"validation_successes"` + ValidationFailures int `json:"validation_failures"` + ValidationChecksPassed int `json:"validation_checks_passed"` + ValidationChecksFailed int `json:"validation_checks_failed"` + LastInterfaceType InterfaceType `json:"last_interface_type"` + FirstRunStartedAt time.Time `json:"first_run_started_at"` + LastRunStartedAt time.Time `json:"last_run_started_at"` + LastRunEndedAt time.Time `json:"last_run_ended_at"` +} + +type TaskRunMetrics struct { + RunID int `json:"run_id"` + TaskName string `json:"task_name"` + InterfaceType InterfaceType `json:"interface_type"` + StartedAt time.Time `json:"started_at"` + EndedAt time.Time `json:"ended_at"` + DurationMs int64 `json:"duration_ms"` + Completed bool `json:"completed"` + ValidationAttempts int `json:"validation_attempts"` + ValidationSuccesses int `json:"validation_successes"` + ValidationFailures int `json:"validation_failures"` + ValidationChecksPassed int `json:"validation_checks_passed"` + ValidationChecksFailed int `json:"validation_checks_failed"` + LastValidationMessage string `json:"last_validation_message,omitempty"` + QuestionnaireCompleted bool `json:"questionnaire_completed"` + QuestionnaireUserQuit bool `json:"questionnaire_user_quit"` + QuestionnaireAnswers map[string]any `json:"questionnaire_answers,omitempty"` + Logs []TaskLogEntry `json:"logs"` + Error string `json:"error,omitempty"` +} + +type TaskLogEntry struct { + Timestamp time.Time `json:"timestamp"` + Level string `json:"level"` + Message string `json:"message"` + Source string `json:"source,omitempty"` + Action string `json:"action,omitempty"` + Target string `json:"target,omitempty"` + Result string `json:"result,omitempty"` + Attempt int `json:"attempt,omitempty"` +} From 9fb56b8643f96344b4415c13022b7241e067578d Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Mon, 2 Mar 2026 23:07:50 +0100 Subject: [PATCH 05/51] add metrics logger to config --- internal/models/config.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/internal/models/config.go b/internal/models/config.go index c5a6ce5..48a7f62 100644 --- a/internal/models/config.go +++ b/internal/models/config.go @@ -7,6 +7,7 @@ type Config struct { BeadsDBPath string IssuePrefix string StatisticsStoragePath string + ActionLogger func(string) } var BaseConfig = Config{ @@ -47,3 +48,8 @@ func (c Config) WithStatisticsStoragePath(path string) Config { c.StatisticsStoragePath = path return c } + +func (c Config) WithActionLogger(logger func(string)) Config { + c.ActionLogger = logger + return c +} From 199369c3bff1ad04b812131ebee4a4fe88bd7092 Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Mon, 2 Mar 2026 23:08:11 +0100 Subject: [PATCH 06/51] record stats --- internal/app/statistics.go | 88 +++++++++++++++++++++++++++++++++++++- 1 file changed, 87 insertions(+), 1 deletion(-) diff --git a/internal/app/statistics.go b/internal/app/statistics.go index 7e00386..42abda5 100644 --- a/internal/app/statistics.go +++ b/internal/app/statistics.go @@ -3,6 +3,9 @@ package app import ( "context" "errors" + "log/slog" + "sync" + "time" "github.com/LazyBachelor/LazyPM/internal/models" "github.com/LazyBachelor/LazyPM/internal/storage" @@ -10,14 +13,17 @@ import ( type StatisticsService struct { storage *storage.Storage[models.Statistics] + logger *slog.Logger + mu sync.Mutex } -func NewStatisticsService(storage *storage.Storage[models.Statistics]) (*StatisticsService, error) { +func NewStatisticsService(storage *storage.Storage[models.Statistics], logger *slog.Logger) (*StatisticsService, error) { if err := storage.Init(); err != nil { return nil, err } return &StatisticsService{ storage: storage, + logger: logger, }, nil } @@ -35,3 +41,83 @@ func (s *StatisticsService) GetStatistics() (models.Statistics, error) { } return *s.storage.Data, nil } + +func (s *StatisticsService) RecordTaskRun(ctx context.Context, run models.TaskRunMetrics) error { + _ = ctx + + s.mu.Lock() + defer s.mu.Unlock() + + if s.storage.Data == nil { + return errors.New("statistics data not initialized") + } + + stats := s.storage.Data + now := time.Now() + + if stats.StartTime.IsZero() { + stats.StartTime = run.StartedAt + } + if run.StartedAt.Before(stats.StartTime) { + stats.StartTime = run.StartedAt + } + + stats.EndTime = now + stats.Duration = stats.EndTime.Sub(stats.StartTime) + stats.InterfaceType = run.InterfaceType + + stats.TaskRuns++ + stats.LastTaskName = run.TaskName + stats.LastRunID = run.RunID + + if run.Completed { + stats.TasksCompleted++ + } else { + stats.TasksFailed++ + } + + stats.TotalDurationMs += run.DurationMs + if stats.TaskRuns > 0 { + stats.AverageDurationMs = stats.TotalDurationMs / int64(stats.TaskRuns) + } + + stats.ValidationAttempts += run.ValidationAttempts + stats.ValidationSuccesses += run.ValidationSuccesses + stats.ValidationFailures += run.ValidationFailures + stats.ValidationChecksPassed += run.ValidationChecksPassed + stats.ValidationChecksFailed += run.ValidationChecksFailed + + userActions := 0 + for _, log := range run.Logs { + if log.Level == "user_action" { + userActions++ + } + } + stats.TotalUserActions += userActions + stats.ButtonClicks.Clicks += userActions + if run.QuestionnaireCompleted { + stats.QuestionnairesCompleted++ + } + if run.QuestionnaireUserQuit { + stats.QuestionnairesAbandoned++ + } + + if err := s.storage.Save(); err != nil { + if s.logger != nil { + s.logger.Error("failed to save global statistics", "error", err) + } + return err + } + + if s.logger != nil { + s.logger.Info("global statistics updated", + "task", run.TaskName, + "run_id", run.RunID, + "task_runs", stats.TaskRuns, + "completed", stats.TasksCompleted, + "failed", stats.TasksFailed, + ) + } + + return nil +} From fb9e8ab419b5d71e39853a4752ae8e767b899fb4 Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Mon, 2 Mar 2026 23:08:40 +0100 Subject: [PATCH 07/51] define metrics logger --- pkg/task/metrics.go | 502 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 502 insertions(+) create mode 100644 pkg/task/metrics.go diff --git a/pkg/task/metrics.go b/pkg/task/metrics.go new file mode 100644 index 0000000..87f4a36 --- /dev/null +++ b/pkg/task/metrics.go @@ -0,0 +1,502 @@ +package task + +import ( + "encoding/json" + "fmt" + "log/slog" + "os" + "path/filepath" + "regexp" + "sort" + "strings" + "sync" + "time" + + "github.com/LazyBachelor/LazyPM/internal/models" +) + +type taskRunCollector struct { + mu sync.Mutex + run models.TaskRunMetrics + logger *slog.Logger +} + +var nonWord = regexp.MustCompile(`[^a-z0-9]+`) + +func newTaskRunCollector(taskName string, interfaceType InterfaceType, logger *slog.Logger) *taskRunCollector { + return &taskRunCollector{ + run: models.TaskRunMetrics{ + TaskName: taskName, + InterfaceType: interfaceType, + StartedAt: time.Now(), + Logs: make([]models.TaskLogEntry, 0, 8), + }, + logger: logger, + } +} + +func (c *taskRunCollector) log(level string, message string) { + action := normalizeAction(message) + result := "" + switch level { + case "error": + result = "failed" + case "warn": + result = "warning" + default: + result = "ok" + } + + c.mu.Lock() + defer c.mu.Unlock() + + c.run.Logs = append(c.run.Logs, models.TaskLogEntry{ + Timestamp: time.Now(), + Level: level, + Message: message, + Source: "system", + Action: action, + Result: result, + }) + + attrs := []any{"action", action, "result", result, "task", c.run.TaskName, "interface", c.run.InterfaceType} + c.logWithLogger(level, message, attrs...) +} + +func (c *taskRunCollector) recordUserAction(raw string) { + source, actionText, target, result := normalizeUserAction(raw) + + c.mu.Lock() + defer c.mu.Unlock() + + c.run.Logs = append(c.run.Logs, models.TaskLogEntry{ + Timestamp: time.Now(), + Level: "user_action", + Message: raw, + Source: source, + Action: normalizeAction(actionText), + Target: target, + Result: result, + }) + + c.logWithLogger("info", "user action recorded", + "task", c.run.TaskName, + "interface", c.run.InterfaceType, + "source", source, + "action", normalizeAction(actionText), + "target", target, + "result", result, + ) +} + +func (c *taskRunCollector) setCompleted(completed bool) { + c.mu.Lock() + defer c.mu.Unlock() + + c.run.Completed = completed +} + +func (c *taskRunCollector) setError(err error) { + if err == nil { + return + } + + c.mu.Lock() + defer c.mu.Unlock() + + c.run.Error = err.Error() +} + +func (c *taskRunCollector) recordQuestionnaire(completed bool, userQuit bool, answers map[string]any) { + c.mu.Lock() + defer c.mu.Unlock() + + c.run.QuestionnaireCompleted = completed + c.run.QuestionnaireUserQuit = userQuit + if len(answers) > 0 { + c.run.QuestionnaireAnswers = answers + } + + result := "completed" + if userQuit { + result = "user_quit" + } else if !completed { + result = "incomplete" + } + + c.run.Logs = append(c.run.Logs, models.TaskLogEntry{ + Timestamp: time.Now(), + Level: "questionnaire", + Message: "questionnaire finished", + Source: "system", + Action: "questionnaire_finish", + Result: result, + }) + + if len(answers) > 0 { + keys := make([]string, 0, len(answers)) + for key := range answers { + keys = append(keys, key) + } + sort.Strings(keys) + + for _, key := range keys { + value := answers[key] + if value == nil { + continue + } + + valueText := fmt.Sprintf("%v", value) + c.run.Logs = append(c.run.Logs, models.TaskLogEntry{ + Timestamp: time.Now(), + Level: "questionnaire", + Message: fmt.Sprintf("questionnaire answer: %s=%s", key, valueText), + Source: "system", + Action: "questionnaire_answer", + Target: key, + Result: valueText, + }) + } + } + + c.logWithLogger("info", "questionnaire recorded", + "task", c.run.TaskName, + "completed", completed, + "user_quit", userQuit, + "answers_count", len(answers), + ) +} + +func (c *taskRunCollector) recordValidation(feedback ValidationFeedback) { + c.mu.Lock() + defer c.mu.Unlock() + + c.run.ValidationAttempts++ + attempt := c.run.ValidationAttempts + if feedback.Success { + c.run.ValidationSuccesses++ + } else { + c.run.ValidationFailures++ + } + + c.run.Logs = append(c.run.Logs, models.TaskLogEntry{ + Timestamp: time.Now(), + Level: "validation", + Message: fmt.Sprintf("validation attempt %d success=%t", attempt, feedback.Success), + Source: "system", + Action: "validate_attempt", + Result: validationResult(feedback.Success), + Attempt: attempt, + }) + c.logWithLogger("info", "validation attempt", + "task", c.run.TaskName, + "interface", c.run.InterfaceType, + "attempt", attempt, + "result", validationResult(feedback.Success), + ) + + for _, check := range feedback.Checks { + if check.Valid { + c.run.ValidationChecksPassed++ + c.run.Logs = append(c.run.Logs, models.TaskLogEntry{ + Timestamp: time.Now(), + Level: "validation", + Message: fmt.Sprintf("validation check passed: %s", check.Message), + Source: "system", + Action: "validate_check", + Target: check.Message, + Result: "passed", + Attempt: attempt, + }) + c.logWithLogger("info", "validation check", + "task", c.run.TaskName, + "attempt", attempt, + "result", "passed", + "target", check.Message, + ) + continue + } + c.run.ValidationChecksFailed++ + c.run.Logs = append(c.run.Logs, models.TaskLogEntry{ + Timestamp: time.Now(), + Level: "validation", + Message: fmt.Sprintf("validation check failed: %s", check.Message), + Source: "system", + Action: "validate_check", + Target: check.Message, + Result: "failed", + Attempt: attempt, + }) + c.logWithLogger("warn", "validation check", + "task", c.run.TaskName, + "attempt", attempt, + "result", "failed", + "target", check.Message, + ) + } + + c.run.LastValidationMessage = feedback.Message +} + +func validationResult(success bool) string { + if success { + return "passed" + } + return "failed" +} + +func normalizeUserAction(raw string) (source string, actionText string, target string, result string) { + trimmed := strings.TrimSpace(raw) + if trimmed == "" { + return "unknown", "unknown_action", "", "unknown" + } + + if event, ok := models.DecodeActionEvent(trimmed); ok { + source = strings.TrimSpace(event.Source) + if source == "" { + source = "unknown" + } + actionText = strings.TrimSpace(event.Action) + if actionText == "" { + actionText = "unknown_action" + } + actionText = stripSourcePrefix(actionText, source) + target = strings.TrimSpace(event.Target) + result = strings.TrimSpace(event.Result) + if result == "" { + result = inferActionResult(actionText) + } + return source, actionText, target, result + } + + result = "ok" + lower := strings.ToLower(trimmed) + + if strings.HasPrefix(lower, "web request:") { + rest := strings.TrimSpace(trimmed[len("web request:"):]) + parts := strings.Fields(rest) + if len(parts) >= 2 { + return "web", "request", strings.ToUpper(parts[0]) + " " + parts[1], "ok" + } + return "web", "request", rest, "ok" + } + + if strings.HasPrefix(lower, "repl command:") { + command := strings.TrimSpace(trimmed[len("repl command:"):]) + return "repl", "run_command", command, "ok" + } + + words := strings.Fields(trimmed) + if len(words) > 1 { + head := strings.ToLower(words[0]) + if head == "tui" || head == "web" || head == "repl" { + source = head + actionText = strings.Join(words[1:], " ") + } else { + source = "unknown" + actionText = trimmed + } + } else { + source = "unknown" + actionText = trimmed + } + + result = inferActionResult(actionText) + + return source, actionText, "", result +} + +func stripSourcePrefix(actionText string, source string) string { + actionText = strings.TrimSpace(actionText) + if actionText == "" || source == "" { + return actionText + } + + lowerSource := strings.ToLower(source) + lowerAction := strings.ToLower(actionText) + + if strings.HasPrefix(lowerAction, lowerSource+" ") { + return strings.TrimSpace(actionText[len(source):]) + } + if strings.HasPrefix(lowerAction, lowerSource+"_") { + return strings.TrimSpace(actionText[len(source)+1:]) + } + + return actionText +} + +func inferActionResult(actionText string) string { + lower := strings.ToLower(actionText) + if strings.Contains(lower, "failed") { + return "failed" + } + if strings.Contains(lower, "canceled") { + return "canceled" + } + if strings.Contains(lower, "started") { + return "started" + } + if strings.Contains(lower, "submitted") { + return "submitted" + } + if strings.Contains(lower, "requested") { + return "requested" + } + return "ok" +} + +func normalizeAction(input string) string { + lower := strings.ToLower(strings.TrimSpace(input)) + if lower == "" { + return "unknown_action" + } + clean := nonWord.ReplaceAllString(lower, "_") + clean = strings.Trim(clean, "_") + if clean == "" { + return "unknown_action" + } + return clean +} + +func (c *taskRunCollector) finalize() models.TaskRunMetrics { + c.mu.Lock() + defer c.mu.Unlock() + + if c.run.EndedAt.IsZero() { + c.run.EndedAt = time.Now() + } + c.run.DurationMs = c.run.EndedAt.Sub(c.run.StartedAt).Milliseconds() + + finalLogs := make([]models.TaskLogEntry, len(c.run.Logs)) + copy(finalLogs, c.run.Logs) + + final := c.run + final.Logs = finalLogs + + return final +} + +func appendTaskMetrics(path string, taskName string, run models.TaskRunMetrics, logger *slog.Logger) error { + if path == "" { + return nil + } + + dir := filepath.Dir(path) + if dir != "." { + if err := os.MkdirAll(dir, 0o755); err != nil { + return fmt.Errorf("failed to create metrics directory: %w", err) + } + } + + metrics := models.TaskMetricsFile{ + TaskName: taskName, + Runs: []models.TaskRunMetrics{}, + } + + if bytes, err := os.ReadFile(path); err == nil { + if len(bytes) > 0 { + if err := json.Unmarshal(bytes, &metrics); err != nil { + return fmt.Errorf("failed to parse metrics file %q: %w", path, err) + } + } + } else if !os.IsNotExist(err) { + return fmt.Errorf("failed to read metrics file %q: %w", path, err) + } + + if metrics.TaskName == "" { + metrics.TaskName = taskName + } + + run.RunID = len(metrics.Runs) + 1 + metrics.Runs = append(metrics.Runs, run) + metrics.Summary = buildTaskStatsSummary(metrics.Runs) + metrics.UpdatedAt = time.Now() + + data, err := json.MarshalIndent(metrics, "", " ") + if err != nil { + return fmt.Errorf("failed to encode task metrics: %w", err) + } + + if err := os.WriteFile(path, data, 0o644); err != nil { + return fmt.Errorf("failed to write metrics file %q: %w", path, err) + } + + if logger != nil { + logger.Info("task metrics persisted", + "path", path, + "task", taskName, + "run_id", run.RunID, + "total_runs", metrics.Summary.TotalRuns, + "completed_runs", metrics.Summary.CompletedRuns, + ) + } + + return nil +} + +func (c *taskRunCollector) logWithLogger(level string, message string, attrs ...any) { + if c.logger == nil { + return + } + + switch level { + case "error": + c.logger.Error(message, attrs...) + case "warn": + c.logger.Warn(message, attrs...) + default: + c.logger.Info(message, attrs...) + } +} + +func buildTaskStatsSummary(runs []models.TaskRunMetrics) models.TaskStatsSummary { + summary := models.TaskStatsSummary{} + if len(runs) == 0 { + return summary + } + + summary.TotalRuns = len(runs) + summary.FirstRunStartedAt = runs[0].StartedAt + + for _, run := range runs { + summary.TotalDurationMs += run.DurationMs + summary.ValidationAttempts += run.ValidationAttempts + summary.ValidationSuccesses += run.ValidationSuccesses + summary.ValidationFailures += run.ValidationFailures + summary.ValidationChecksPassed += run.ValidationChecksPassed + summary.ValidationChecksFailed += run.ValidationChecksFailed + + if run.Completed { + summary.CompletedRuns++ + } else { + summary.IncompleteRuns++ + } + + if run.QuestionnaireCompleted { + summary.QuestionnairesCompleted++ + } + if run.QuestionnaireUserQuit { + summary.QuestionnairesAbandoned++ + } + + if run.StartedAt.Before(summary.FirstRunStartedAt) { + summary.FirstRunStartedAt = run.StartedAt + } + if run.StartedAt.After(summary.LastRunStartedAt) { + summary.LastRunStartedAt = run.StartedAt + } + if run.EndedAt.After(summary.LastRunEndedAt) { + summary.LastRunEndedAt = run.EndedAt + summary.LastInterfaceType = run.InterfaceType + } + + for _, log := range run.Logs { + if log.Level == "user_action" { + summary.TotalUserActions++ + } + } + } + + summary.AverageDurationMs = summary.TotalDurationMs / int64(summary.TotalRuns) + return summary +} From 04191b5a976124cc7f28cd7d2c468b1caebb1795 Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Mon, 2 Mar 2026 23:09:03 +0100 Subject: [PATCH 08/51] we need app --- cmd/pm/runner.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cmd/pm/runner.go b/cmd/pm/runner.go index 3da601e..d181c17 100644 --- a/cmd/pm/runner.go +++ b/cmd/pm/runner.go @@ -55,13 +55,13 @@ func runStartCmd(cmd *cobra.Command, args []string) error { return returnIfUserQuit(err, "failed to run intro") } - if err := taskLoop(cmd.Context(), surveyTasks, interfaces); err != nil { + if err := taskLoop(cmd.Context(), app, surveyTasks, interfaces); err != nil { return returnIfUserQuit(err, "task loop failed") } return nil } -func taskLoop(ctx context.Context, surveyTasks map[string]task.Tasker, interfaces map[string]task.Interface) error { +func taskLoop(ctx context.Context, application *models.App, surveyTasks map[string]task.Tasker, interfaces map[string]task.Interface) error { var iNames []string for name := range interfaces { iNames = append(iNames, name) @@ -84,7 +84,7 @@ func taskLoop(ctx context.Context, surveyTasks map[string]task.Tasker, interface iIdx := idx % len(iNames) selected := interfaces[iNames[iIdx]] - if err := task.RunTask(ctx, t, selected, tasks.InterfaceToType(selected)); err != nil { + if err := task.RunTask(ctx, application, t, selected, tasks.InterfaceToType(selected)); err != nil { return err } idx++ From 35557431da55ec3d8b8cff140a21fc98d6bc23ae Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Mon, 2 Mar 2026 23:09:25 +0100 Subject: [PATCH 09/51] add logging and metrics to web --- pkg/web/server/routes.go | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/pkg/web/server/routes.go b/pkg/web/server/routes.go index 11e90c5..5ed3637 100644 --- a/pkg/web/server/routes.go +++ b/pkg/web/server/routes.go @@ -9,6 +9,7 @@ import ( "strings" "time" + "github.com/LazyBachelor/LazyPM/internal/models" "github.com/LazyBachelor/LazyPM/pkg/web/handler" "github.com/NYTimes/gziphandler" "github.com/go-chi/chi/v5" @@ -26,6 +27,7 @@ func (s *Server) RegisterRoutes(assets embed.FS) http.Handler { r.Use(handler.HTMXMiddleware) r.Use(handler.AppMiddleware(s.App)) + r.Use(actionLoggingMiddleware(s.App)) s.handleAssets(r, assets) @@ -56,6 +58,35 @@ func (s *Server) RegisterRoutes(assets embed.FS) http.Handler { return gziphandler.GzipHandler(r) } +func actionLoggingMiddleware(app interface{ LogAction(string) }) func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if app != nil && shouldLogWebAction(r.URL.Path) { + app.LogAction(models.EncodeActionEvent(models.ActionEvent{ + Source: "web", + Action: "request", + Target: r.Method + " " + r.URL.Path, + Result: "ok", + })) + } + next.ServeHTTP(w, r) + }) + } +} + +func shouldLogWebAction(path string) bool { + if strings.HasPrefix(path, "/assets/") { + return false + } + + switch path { + case "/status": + return false + default: + return true + } +} + func (s *Server) handleAssets(r chi.Router, assets embed.FS) { var fileServer http.Handler From 5ebe95b62f2b38ef04cd5d92b3efa7a9de058c22 Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Mon, 2 Mar 2026 23:09:49 +0100 Subject: [PATCH 10/51] add metrics to tasks --- cmd/pm/tasks/base.go | 2 + cmd/pm/tasks/codingTask.go | 21 ++++++-- cmd/pm/tasks/createIssue.go | 5 ++ cmd/pm/tasks/gitTask.go | 6 +++ pkg/task/questionnaire.go | 27 +++++++++- pkg/task/runner.go | 102 +++++++++++++++++++++++++++++++++--- 6 files changed, 151 insertions(+), 12 deletions(-) diff --git a/cmd/pm/tasks/base.go b/cmd/pm/tasks/base.go index a00122b..3736c32 100644 --- a/cmd/pm/tasks/base.go +++ b/cmd/pm/tasks/base.go @@ -73,10 +73,12 @@ func BaseQuestions(interfaceType InterfaceType) Questions { return Questions{ huh.NewGroup( huh.NewConfirm(). + Key("task_completed"). Title("Did you complete the task?"), ), huh.NewGroup( huh.NewSelect[int]().Value(&taskRating). + Key("task_difficulty"). Options( huh.NewOption("Very easy", 1), huh.NewOption("Easy", 2), diff --git a/cmd/pm/tasks/codingTask.go b/cmd/pm/tasks/codingTask.go index 24165f0..5b7d3e0 100644 --- a/cmd/pm/tasks/codingTask.go +++ b/cmd/pm/tasks/codingTask.go @@ -51,22 +51,35 @@ func (t *CodingTask) Questions(interfaceType InterfaceType) (questions Questions return BaseQuestions(interfaceType). With( ReplQuestion(interfaceType, - huh.NewConfirm().Title("Question only for REPL interface")), + huh.NewConfirm().Key("repl_experience").Title("Question only for REPL interface")), ). With( WebQuestion(interfaceType, - huh.NewInput().Title("Question only for Web interface")), + huh.NewInput().Key("web_feedback").Title("Question only for Web interface")), ). With( TUIQuestion(interfaceType, - huh.NewConfirm().Title("Question only for TUI interface")), + huh.NewConfirm().Key("tui_experience").Title("Question only for TUI interface")), ). With( Question( - huh.NewConfirm().Title("One last question for all interfaces!")), + huh.NewConfirm().Key("final_confirmation").Title("One last question for all interfaces!")), ) } +func (t *CodingTask) QuestionnaireKeys(interfaceType InterfaceType) []string { + keys := []string{"task_completed", "task_difficulty", "final_confirmation"} + switch interfaceType { + case InterfaceTypeREPL: + keys = append(keys, "repl_experience") + case InterfaceTypeWeb: + keys = append(keys, "web_feedback") + case InterfaceTypeTUI: + keys = append(keys, "tui_experience") + } + return keys +} + func (t *CodingTask) Setup(ctx context.Context) error { if err := ClearIssues(t.app); err != nil { return err diff --git a/cmd/pm/tasks/createIssue.go b/cmd/pm/tasks/createIssue.go index b10c32c..d931b0a 100644 --- a/cmd/pm/tasks/createIssue.go +++ b/cmd/pm/tasks/createIssue.go @@ -42,6 +42,11 @@ func (t *CreateIssueTask) Questions(interfaceType InterfaceType) Questions { return BaseQuestions(interfaceType) } +func (t *CreateIssueTask) QuestionnaireKeys(interfaceType InterfaceType) []string { + _ = interfaceType + return []string{"task_completed", "task_difficulty"} +} + func (t *CreateIssueTask) Setup(ctx context.Context) error { if err := ClearIssues(t.app); err != nil { return err diff --git a/cmd/pm/tasks/gitTask.go b/cmd/pm/tasks/gitTask.go index 12f39c2..6520339 100644 --- a/cmd/pm/tasks/gitTask.go +++ b/cmd/pm/tasks/gitTask.go @@ -47,6 +47,7 @@ func (t *GitTask) Questions(interfaceType InterfaceType) Questions { return BaseQuestions(interfaceType).With( huh.NewGroup( huh.NewSelect[string]().Title("What Git Interface did you use?"). + Key("git_interface_used"). Options( huh.Option[string]{Value: "cli", Key: "Command Line Interface"}, huh.Option[string]{Value: "tui", Key: "Terminal User Interface"}, @@ -56,6 +57,11 @@ func (t *GitTask) Questions(interfaceType InterfaceType) Questions { ) } +func (t *GitTask) QuestionnaireKeys(interfaceType InterfaceType) []string { + _ = interfaceType + return []string{"task_completed", "task_difficulty", "git_interface_used"} +} + func (t *GitTask) Setup(ctx context.Context) error { if err := ClearIssues(t.app); err != nil { return err diff --git a/pkg/task/questionnaire.go b/pkg/task/questionnaire.go index 09f8490..369ef08 100644 --- a/pkg/task/questionnaire.go +++ b/pkg/task/questionnaire.go @@ -13,17 +13,19 @@ type Questions = models.Questions type QuestionnaireModel struct { Questions form *huh.Form + keys []string width, height int userQuit bool } -func NewQuestionnaireModel(questions Questions) *QuestionnaireModel { +func NewQuestionnaireModel(questions Questions, keys []string) *QuestionnaireModel { form := huh.NewForm(questions...). WithTheme(style.HuhCenterTheme()).WithLayout(huh.LayoutGrid(1, 1)) return &QuestionnaireModel{ Questions: questions, form: form, + keys: keys, } } @@ -72,3 +74,26 @@ func (q *QuestionnaireModel) SetSize(width, height int) { func (q QuestionnaireModel) GetUserQuit() bool { return q.userQuit } + +func (q QuestionnaireModel) GetCompleted() bool { + return q.form != nil && q.form.State == huh.StateCompleted +} + +func (q QuestionnaireModel) GetAnswers() map[string]any { + if q.form == nil || len(q.keys) == 0 { + return nil + } + + answers := make(map[string]any) + for _, key := range q.keys { + if key == "" { + continue + } + answers[key] = q.form.Get(key) + } + if len(answers) == 0 { + return nil + } + + return answers +} diff --git a/pkg/task/runner.go b/pkg/task/runner.go index a9c4389..9642366 100644 --- a/pkg/task/runner.go +++ b/pkg/task/runner.go @@ -3,6 +3,7 @@ package task import ( "context" "fmt" + "log/slog" "time" "github.com/LazyBachelor/LazyPM/internal/models" @@ -20,6 +21,10 @@ type InterfaceType = models.InterfaceType type ValidatedInterface = models.ValidatedInterface type ValidationFeedback = models.ValidationFeedback +type QuestionnaireKeysProvider interface { + QuestionnaireKeys(InterfaceType) []string +} + var ErrUserQuit = models.ErrUserQuit // RunTask orchestrates the complete task execution flow: @@ -28,7 +33,53 @@ var ErrUserQuit = models.ErrUserQuit // 3. Run the interface // 4. Start validation loop in background // 5. Show questionnaire when done -func RunTask(ctx context.Context, t Tasker, i Interface, iType InterfaceType) error { +func RunTask(ctx context.Context, app *App, t Tasker, i Interface, iType InterfaceType) (runErr error) { + config := t.Config() + details := t.Details() + + var logger *slog.Logger + if app != nil { + logger = app.Logger + } + + collector := newTaskRunCollector(details.Title, iType, logger) + collector.log("info", "task run started") + + config = config.WithActionLogger(func(action string) { + collector.recordUserAction(action) + }) + + 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) + } + } + } + }() + doneChan := make(chan bool, 1) quitChan := make(chan bool, 1) feedbackChan := make(chan ValidationFeedback, 10) @@ -38,60 +89,94 @@ func RunTask(ctx context.Context, t Tasker, i Interface, iType InterfaceType) er } // 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") // Show task intro - detailsScreen := NewTaskModel(t.Details()) + 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") // Start validation loop - go startValidationLoop(ctx, t, feedbackChan, doneChan, quitChan) + collector.log("info", "starting validation loop") + go startValidationLoop(ctx, t, feedbackChan, doneChan, quitChan, collector.recordValidation) // Run interface + collector.log("info", "starting task interface") interfaceDone := make(chan error, 1) go func() { - interfaceDone <- i.Run(ctx, t.Config()) + interfaceDone <- i.Run(ctx, config) }() select { case <-doneChan: close(quitChan) + collector.setCompleted(true) + collector.log("info", "task validation completed") if err := <-interfaceDone; err != nil { - fmt.Printf("warning: interface error after task completion: %v\n", err) + 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: 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) } + 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) - questionare := NewQuestionnaireModel(questions) + questionnaireKeys := []string{} + if provider, ok := t.(QuestionnaireKeysProvider); ok { + questionnaireKeys = provider.QuestionnaireKeys(iType) + } + questionare := NewQuestionnaireModel(questions, questionnaireKeys) model, err = tea.NewProgram(questionare, 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 && 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) { +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() @@ -99,6 +184,9 @@ func startValidationLoop(ctx context.Context, t Tasker, feedbackChan chan Valida select { case <-ticker.C: feedback := t.Validate(ctx) + if onFeedback != nil { + onFeedback(feedback) + } if feedback.Success { feedback.Message = "Task completed successfully!" feedbackChan <- feedback From 5942d0d8ab803ec8315dd8ceed3acec4e138b382 Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Mon, 2 Mar 2026 23:09:59 +0100 Subject: [PATCH 11/51] add metrics to repl --- pkg/repl/repl.go | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/pkg/repl/repl.go b/pkg/repl/repl.go index 0c999f0..091d0f0 100644 --- a/pkg/repl/repl.go +++ b/pkg/repl/repl.go @@ -100,10 +100,15 @@ func (r *REPL) Run(ctx context.Context, config app.Config) error { // If the user types "exit" or "quit", break the loop and exit the REPL. if input == "exit" || input == "quit" { + r.logAction("repl exit requested") fmt.Println("Goodbye!") break } + if input != "" { + r.logAction("repl command: " + input) + } + // Add the input to the history for future navigation. history = append(history, input) @@ -144,3 +149,12 @@ func (r *REPL) SetChannels(feedbackChan chan task.ValidationFeedback, quitChan c r.feedbackChan = feedbackChan r.quitChan = quitChan } + +func (r *REPL) logAction(action string) { + if r.app != nil { + r.app.LogAction(models.EncodeActionEvent(models.ActionEvent{ + Source: "repl", + Action: action, + })) + } +} From 7c704a1572a4c033eb643175e09de5c0dabda4fb Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Mon, 2 Mar 2026 23:10:09 +0100 Subject: [PATCH 12/51] add metrics to tui --- pkg/tui/views/dashboard/keys.go | 14 +++++++++++ pkg/tui/views/dashboard/model.go | 19 +++++++++++---- pkg/tui/views/dashboard/operations.go | 34 +++++++++++++++++++++++++++ 3 files changed, 62 insertions(+), 5 deletions(-) diff --git a/pkg/tui/views/dashboard/keys.go b/pkg/tui/views/dashboard/keys.go index 34760f5..557a64e 100644 --- a/pkg/tui/views/dashboard/keys.go +++ b/pkg/tui/views/dashboard/keys.go @@ -87,47 +87,61 @@ func (d *Model) handleKeyMsg(msg tea.KeyMsg) tea.Cmd { switch { case key.Matches(msg, d.keyMap.Help): d.helpBar.ToggleHelp() + d.logAction("tui toggled help") case key.Matches(msg, d.keyMap.Quit): + d.logAction("tui quit requested") return tea.Quit case key.Matches(msg, d.keyMap.SwitchWindow): d.ToggleFocusedWindow() + d.logAction("tui switched issue window") case d.IsFocusedOnList() && key.Matches(msg, d.keyMap.SelectIssue): d.FocusDetail() + d.logAction("tui opened issue detail") case d.IsFocusedOnDetail() && key.Matches(msg, d.keyMap.BackToList): d.FocusList() + d.logAction("tui returned to issue list") case d.IsFocusedOnDetail() && key.Matches(msg, d.keyMap.ScrollUp): d.issueDetail.ScrollUp(1) + d.logAction("tui scrolled issue detail up") case d.IsFocusedOnDetail() && key.Matches(msg, d.keyMap.ScrollDown): d.issueDetail.ScrollDown(1) + d.logAction("tui scrolled issue detail down") case !d.editingTitle && !d.creatingIssue && !d.editingDescription && !d.choosingStatus && !d.choosingPriority && !d.confirmingDelete && !d.choosingType && key.Matches(msg, d.keyMap.EditTitle): if selected := d.FocusedIssueList().SelectedItem(); selected.ID != "" { d.startEditTitle(selected) cmd = d.titleInput.Focus() + d.logAction("tui started editing issue title") } case !d.editingTitle && !d.creatingIssue && !d.editingDescription && !d.choosingStatus && !d.choosingPriority && !d.confirmingDelete && !d.choosingType && key.Matches(msg, d.keyMap.EditDescription): if selected := d.FocusedIssueList().SelectedItem(); selected.ID != "" { d.startEditDescription(selected) cmd = d.descriptionInput.Focus() + d.logAction("tui started editing issue description") } case !d.editingTitle && !d.creatingIssue && !d.editingDescription && !d.choosingStatus && !d.choosingPriority && !d.confirmingDelete && !d.choosingType && key.Matches(msg, d.keyMap.ChangeStatus): if selected := d.FocusedIssueList().SelectedItem(); selected.ID != "" { d.startChooseStatus(selected) + d.logAction("tui opened status picker") } case !d.editingTitle && !d.creatingIssue && !d.editingDescription && !d.choosingStatus && !d.choosingPriority && !d.confirmingDelete && !d.choosingType && key.Matches(msg, d.keyMap.ChangePriority): if selected := d.FocusedIssueList().SelectedItem(); selected.ID != "" { d.startChoosePriority(selected) + d.logAction("tui opened priority picker") } case !d.editingTitle && !d.creatingIssue && !d.editingDescription && !d.choosingStatus && !d.choosingPriority && !d.confirmingDelete && !d.choosingType && key.Matches(msg, d.keyMap.ChangeType): if selected := d.FocusedIssueList().SelectedItem(); selected.ID != "" { d.startChooseType(selected) + d.logAction("tui opened type picker") } case !d.editingTitle && !d.creatingIssue && !d.editingDescription && !d.choosingStatus && !d.choosingPriority && !d.confirmingDelete && !d.choosingType && key.Matches(msg, d.keyMap.AddIssue): d.startCreateIssue() cmd = d.createTitleInput.Focus() + d.logAction("tui started creating issue") case !d.editingTitle && !d.creatingIssue && !d.editingDescription && !d.choosingStatus && !d.choosingPriority && !d.confirmingDelete && !d.choosingType && key.Matches(msg, d.keyMap.DeleteIssue): fl := d.FocusedIssueList() if selected := fl.SelectedItem(); selected.ID != "" { d.startConfirmDelete(selected.ID, fl.Index()) + d.logAction("tui opened delete confirmation") } } diff --git a/pkg/tui/views/dashboard/model.go b/pkg/tui/views/dashboard/model.go index dde0979..4b98ec0 100644 --- a/pkg/tui/views/dashboard/model.go +++ b/pkg/tui/views/dashboard/model.go @@ -46,11 +46,11 @@ type Model struct { choosingPriority bool // true while choosing a priority priorityIssueID string choosingType bool // true while choosing a type - typeIssueID string - feedbackChan chan models.ValidationFeedback - quitChan chan bool - currentFeedback models.ValidationFeedback - showComplete bool + typeIssueID string + feedbackChan chan models.ValidationFeedback + quitChan chan bool + currentFeedback models.ValidationFeedback + showComplete bool } func NewDashboard(app *app.App, feedbackChan chan models.ValidationFeedback, quitChan chan bool) *Model { @@ -98,6 +98,15 @@ func NewDashboard(app *app.App, feedbackChan chan models.ValidationFeedback, qui return m } +func (m *Model) logAction(action string) { + if m.app != nil { + m.app.LogAction(models.EncodeActionEvent(models.ActionEvent{ + Source: "tui", + Action: action, + })) + } +} + func (m *Model) startEditTitle(selected ListIssue) { m.editingTitle = true m.editingIssueID = selected.ID diff --git a/pkg/tui/views/dashboard/operations.go b/pkg/tui/views/dashboard/operations.go index 8d53ee9..6a5a038 100644 --- a/pkg/tui/views/dashboard/operations.go +++ b/pkg/tui/views/dashboard/operations.go @@ -135,8 +135,10 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.editingIssueID = "" m.titleInput.Blur() if msg.Err != nil { + m.logAction("tui failed to update issue title") return m, nil } + m.logAction("tui updated issue title") return m, m.refreshIssueListsAndSelectIssue(msg.IssueID) case issueDescriptionUpdatedMsg: @@ -144,32 +146,40 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.editingDescIssueID = "" m.descriptionInput.Blur() if msg.Err != nil { + m.logAction("tui failed to update issue description") return m, nil } + m.logAction("tui updated issue description") return m, m.refreshIssueListsAndSelectIssue(msg.IssueID) case issueStatusUpdatedMsg: m.choosingStatus = false m.statusIssueID = "" if msg.Err != nil { + m.logAction("tui failed to update issue status") return m, nil } + m.logAction("tui updated issue status") return m, m.refreshIssueListsAndSelectIssue(msg.IssueID) case issuePriorityUpdatedMsg: m.choosingPriority = false m.priorityIssueID = "" if msg.Err != nil { + m.logAction("tui failed to update issue priority") return m, nil } + m.logAction("tui updated issue priority") return m, m.refreshIssueListsAndSelectIssue(msg.IssueID) case issueTypeUpdatedMsg: m.choosingType = false m.typeIssueID = "" if msg.Err != nil { + m.logAction("tui failed to update issue type") return m, nil } + m.logAction("tui updated issue type") return m, m.refreshIssueListsAndSelectIssue(msg.IssueID) case selectIssueMsg: @@ -182,6 +192,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.createTitleInput.Blur() m.createTitleInput.Reset() if msg.Err != nil || msg.Issue == nil { + m.logAction("tui failed to create issue") return m, nil } issues, err := m.app.Issues.SearchIssues(context.Background(), "", models.IssueFilter{}) @@ -204,13 +215,16 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } m.issueDetail.SetIssue(*selectedIssue) + m.logAction("tui created issue") return m, tea.Sequence(setItemsCmd, closedSetCmd, func() tea.Msg { return selectIssueMsg{IssueID: selectedIssue.ID} }) case issueDeletedMsg: m.confirmingDelete = false m.deleteConfirmID = "" if msg.Err != nil { + m.logAction("tui failed to delete issue") return m, nil } + m.logAction("tui deleted issue") issues, err := m.app.Issues.SearchIssues(context.Background(), "", models.IssueFilter{}) if err != nil { return m, nil @@ -263,12 +277,14 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if m.confirmingDelete { switch msg.String() { case "y", "Y": + m.logAction("tui confirmed issue deletion") issueID := m.deleteConfirmID idx := m.deleteConfirmIndex m.confirmingDelete = false m.deleteConfirmID = "" return m, deleteIssueCmd(m.app, issueID, idx) case "n", "N", "esc": + m.logAction("tui canceled issue deletion") m.confirmingDelete = false m.deleteConfirmID = "" return m, nil @@ -278,21 +294,25 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if m.choosingStatus { switch msg.String() { case "o": + m.logAction("tui selected issue status open") issueID := m.statusIssueID m.choosingStatus = false m.statusIssueID = "" return m, updateIssueStatusCmd(m.app, issueID, string(models.StatusOpen)) case "i": + m.logAction("tui selected issue status in_progress") issueID := m.statusIssueID m.choosingStatus = false m.statusIssueID = "" return m, updateIssueStatusCmd(m.app, issueID, string(models.StatusInProgress)) case "c": + m.logAction("tui selected issue status closed") issueID := m.statusIssueID m.choosingStatus = false m.statusIssueID = "" return m, updateIssueStatusCmd(m.app, issueID, string(models.StatusClosed)) case "esc": + m.logAction("tui canceled status picker") m.choosingStatus = false m.statusIssueID = "" return m, nil @@ -302,12 +322,14 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if m.choosingPriority { switch msg.String() { case "0", "1", "2", "3", "4": + m.logAction("tui selected issue priority") issueID := m.priorityIssueID priority := int(msg.String()[0] - '0') m.choosingPriority = false m.priorityIssueID = "" return m, updateIssuePriorityCmd(m.app, issueID, priority) case "esc": + m.logAction("tui canceled priority picker") m.choosingPriority = false m.priorityIssueID = "" return m, nil @@ -319,31 +341,37 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if m.choosingType { switch msg.String() { case "b": + m.logAction("tui selected issue type bug") issueID := m.typeIssueID m.choosingType = false m.typeIssueID = "" return m, updateIssueTypeCmd(m.app, issueID, models.TypeBug) case "f": + m.logAction("tui selected issue type feature") issueID := m.typeIssueID m.choosingType = false m.typeIssueID = "" return m, updateIssueTypeCmd(m.app, issueID, models.TypeFeature) case "t": + m.logAction("tui selected issue type task") issueID := m.typeIssueID m.choosingType = false m.typeIssueID = "" return m, updateIssueTypeCmd(m.app, issueID, models.TypeTask) case "e": + m.logAction("tui selected issue type epic") issueID := m.typeIssueID m.choosingType = false m.typeIssueID = "" return m, updateIssueTypeCmd(m.app, issueID, models.TypeEpic) case "c": + m.logAction("tui selected issue type chore") issueID := m.typeIssueID m.choosingType = false m.typeIssueID = "" return m, updateIssueTypeCmd(m.app, issueID, models.TypeChore) case "esc": + m.logAction("tui canceled type picker") m.choosingType = false m.typeIssueID = "" return m, nil @@ -356,10 +384,12 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if msg.String() == "enter" { title := m.createTitleInput.Value() if title != "" { + m.logAction("tui submitted new issue") return m, createIssueCmd(m.app, title) } } if msg.String() == "esc" { + m.logAction("tui canceled issue creation") m.creatingIssue = false m.createTitleInput.Blur() m.createTitleInput.Reset() @@ -374,10 +404,12 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if msg.String() == "enter" { newTitle := m.titleInput.Value() if newTitle != "" { + m.logAction("tui submitted issue title edit") return m, updateIssueTitleCmd(m.app, m.editingIssueID, newTitle) } } if msg.String() == "esc" { + m.logAction("tui canceled issue title edit") m.editingTitle = false m.editingIssueID = "" m.titleInput.Blur() @@ -390,6 +422,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if m.editingDescription { if msg.String() == "ctrl+s" { + m.logAction("tui submitted issue description edit") issueID := m.editingDescIssueID newDesc := m.descriptionInput.Value() m.editingDescription = false @@ -398,6 +431,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, updateIssueDescriptionCmd(m.app, issueID, newDesc) } if msg.String() == "esc" { + m.logAction("tui canceled issue description edit") m.editingDescription = false m.editingDescIssueID = "" m.descriptionInput.Blur() From c36599c96a58029eb490766808b5e039b64fec3a Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Tue, 3 Mar 2026 10:46:37 +0100 Subject: [PATCH 13/51] reduce deps --- cmd/pm/runner.go | 7 +++---- cmd/pm/tasks/base.go | 4 ++-- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/cmd/pm/runner.go b/cmd/pm/runner.go index d181c17..d06d045 100644 --- a/cmd/pm/runner.go +++ b/cmd/pm/runner.go @@ -7,8 +7,7 @@ import ( "math/rand" "github.com/LazyBachelor/LazyPM/cmd/pm/tasks" - survey "github.com/LazyBachelor/LazyPM/internal/commands/survey" - "github.com/LazyBachelor/LazyPM/internal/models" + "github.com/LazyBachelor/LazyPM/internal/commands/survey" "github.com/LazyBachelor/LazyPM/pkg/task" "github.com/spf13/cobra" ) @@ -61,7 +60,7 @@ func runStartCmd(cmd *cobra.Command, args []string) error { return nil } -func taskLoop(ctx context.Context, application *models.App, surveyTasks map[string]task.Tasker, interfaces map[string]task.Interface) error { +func taskLoop(ctx context.Context, application *task.App, surveyTasks map[string]task.Tasker, interfaces map[string]task.Interface) error { var iNames []string for name := range interfaces { iNames = append(iNames, name) @@ -93,7 +92,7 @@ func taskLoop(ctx context.Context, application *models.App, surveyTasks map[stri } func returnIfUserQuit(err error, msg string) error { - if errors.Is(err, models.ErrUserQuit) { + if errors.Is(err, task.ErrUserQuit) { return nil } return fmt.Errorf("%s: %w", msg, err) diff --git a/cmd/pm/tasks/base.go b/cmd/pm/tasks/base.go index 3736c32..ca78479 100644 --- a/cmd/pm/tasks/base.go +++ b/cmd/pm/tasks/base.go @@ -117,13 +117,13 @@ func TUIQuestion(interfaceType InterfaceType, fields ...huh.Field) *huh.Group { // FetchIssues retrieves all issues from the app and returns those that are relevant for validation, // excluding the setup issue. It also updates the setup issue with the latest data from the app. -func FetchIssues(ctx context.Context, app *app.App, setupIssue *models.Issue) ([]*models.Issue, error) { +func FetchIssues(ctx context.Context, app *App, setupIssue *Issue) ([]*Issue, error) { issues, err := app.Issues.SearchIssues(ctx, "", models.IssueFilter{}) if err != nil { return nil, err } - var relevantIssues []*models.Issue + var relevantIssues []*Issue for _, issue := range issues { if issue.ID != setupIssue.ID { relevantIssues = append(relevantIssues, issue) From e8d9f9d1b6c6317157b0c8f6122e1e2828cd90f9 Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Tue, 3 Mar 2026 10:50:15 +0100 Subject: [PATCH 14/51] need this for issue id completion --- cmd/pm/init.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cmd/pm/init.go b/cmd/pm/init.go index 322a2b2..5ba5040 100644 --- a/cmd/pm/init.go +++ b/cmd/pm/init.go @@ -6,8 +6,8 @@ import ( "github.com/LazyBachelor/LazyPM/cmd/pm/tasks" "github.com/LazyBachelor/LazyPM/internal/app" - issues "github.com/LazyBachelor/LazyPM/internal/commands/issues" - survey "github.com/LazyBachelor/LazyPM/internal/commands/survey" + "github.com/LazyBachelor/LazyPM/internal/commands/issues" + "github.com/LazyBachelor/LazyPM/internal/commands/survey" "github.com/LazyBachelor/LazyPM/pkg/repl" "github.com/LazyBachelor/LazyPM/pkg/task" "github.com/LazyBachelor/LazyPM/pkg/tui" @@ -149,7 +149,7 @@ func setupLazyInitialization() { func commandNeedsApp(cmd *cobra.Command) bool { name := cmd.Name() switch name { - case "help", "completion", "__complete", "__completeNoDesc": + case "help", "completion": return false } From 763f079063244c62e43b45cdfc832fbd7ec8f80f Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Tue, 3 Mar 2026 11:41:07 +0100 Subject: [PATCH 15/51] refactor for separation of concern for better readabiliy and maintainability --- cmd/pm/runner.go | 4 +- internal/app/initializer.go | 2 - internal/app/options.go | 4 +- internal/app/statistics.go | 6 +- .../{issueBuilder.go => issue_builder.go} | 0 pkg/task/lifecsycle.go | 39 ++ pkg/task/metrics.go | 485 ++++++------------ pkg/task/metrics_store.go | 87 ++++ pkg/task/metrics_summary.go | 72 +++ pkg/task/runner.go | 257 +++++----- pkg/task/validation.go | 45 ++ 11 files changed, 530 insertions(+), 471 deletions(-) rename internal/models/{issueBuilder.go => issue_builder.go} (100%) create mode 100644 pkg/task/lifecsycle.go create mode 100644 pkg/task/metrics_store.go create mode 100644 pkg/task/metrics_summary.go create mode 100644 pkg/task/validation.go diff --git a/cmd/pm/runner.go b/cmd/pm/runner.go index d06d045..1c14bae 100644 --- a/cmd/pm/runner.go +++ b/cmd/pm/runner.go @@ -83,7 +83,9 @@ func taskLoop(ctx context.Context, application *task.App, surveyTasks map[string iIdx := idx % len(iNames) selected := interfaces[iNames[iIdx]] - if err := task.RunTask(ctx, application, t, selected, tasks.InterfaceToType(selected)); err != nil { + runner := task.NewTaskRunner(application) + + if err := runner.Run(ctx, t, selected, tasks.InterfaceToType(selected)); err != nil { return err } idx++ diff --git a/internal/app/initializer.go b/internal/app/initializer.go index 6b883ab..c523275 100644 --- a/internal/app/initializer.go +++ b/internal/app/initializer.go @@ -16,11 +16,9 @@ type InteractiveInitializer struct{} func (i InteractiveInitializer) Init(path string) error { _, err := os.Stat(path) if err == nil { - // Path exists; assume already initialized. return nil } if !os.IsNotExist(err) { - // Some other filesystem error; propagate it to the caller. return err } diff --git a/internal/app/options.go b/internal/app/options.go index d288c28..86d4341 100644 --- a/internal/app/options.go +++ b/internal/app/options.go @@ -26,7 +26,7 @@ type AppBuilder struct { func defaultBuilder(ctx context.Context, config Config) *AppBuilder { lifecycle := NewLifecycle() - logger := newDefaultLogger(config, lifecycle) + logger := defaultLogger(config, lifecycle) return &AppBuilder{ ctx: ctx, @@ -37,7 +37,7 @@ func defaultBuilder(ctx context.Context, config Config) *AppBuilder { } } -func newDefaultLogger(config Config, lifecycle *Lifecycle) *slog.Logger { +func defaultLogger(config Config, lifecycle *Lifecycle) *slog.Logger { statsDir := filepath.Dir(config.StatisticsStoragePath) if statsDir == "" { statsDir = "." diff --git a/internal/app/statistics.go b/internal/app/statistics.go index 42abda5..fce991d 100644 --- a/internal/app/statistics.go +++ b/internal/app/statistics.go @@ -2,7 +2,7 @@ package app import ( "context" - "errors" + "fmt" "log/slog" "sync" "time" @@ -37,7 +37,7 @@ func (s *StatisticsService) Save(ctx context.Context) error { func (s *StatisticsService) GetStatistics() (models.Statistics, error) { if s.storage.Data == nil { - return models.Statistics{}, errors.New("statistics data not initialized") + return models.Statistics{}, fmt.Errorf("statistics data not initialized") } return *s.storage.Data, nil } @@ -49,7 +49,7 @@ func (s *StatisticsService) RecordTaskRun(ctx context.Context, run models.TaskRu defer s.mu.Unlock() if s.storage.Data == nil { - return errors.New("statistics data not initialized") + return fmt.Errorf("statistics data not initialized") } stats := s.storage.Data diff --git a/internal/models/issueBuilder.go b/internal/models/issue_builder.go similarity index 100% rename from internal/models/issueBuilder.go rename to internal/models/issue_builder.go diff --git a/pkg/task/lifecsycle.go b/pkg/task/lifecsycle.go new file mode 100644 index 0000000..8a9fa29 --- /dev/null +++ b/pkg/task/lifecsycle.go @@ -0,0 +1,39 @@ +package task + +import ( + "log/slog" + + "github.com/LazyBachelor/LazyPM/internal/models" +) + +type RunLifecycle struct { + collector *taskRunCollector + config Config + details models.TaskDetails + app *App + logger *slog.Logger + metricsStore MetricsStore +} + +func NewRunLifecycle(app *App, config Config, details models.TaskDetails, iType InterfaceType, logger *slog.Logger) *RunLifecycle { + + collector := newTaskRunCollector(details.Title, iType, logger) + + config = config.WithActionLogger(func(action string) { + collector.recordUserAction(action) + }) + + var store MetricsStore + if config.StatisticsStoragePath != "" { + store = NewFileMetricsStore(config.StatisticsStoragePath, logger) + } + + return &RunLifecycle{ + collector: collector, + config: config, + details: details, + app: app, + logger: logger, + metricsStore: store, + } +} diff --git a/pkg/task/metrics.go b/pkg/task/metrics.go index 87f4a36..19990c9 100644 --- a/pkg/task/metrics.go +++ b/pkg/task/metrics.go @@ -1,11 +1,8 @@ package task import ( - "encoding/json" "fmt" "log/slog" - "os" - "path/filepath" "regexp" "sort" "strings" @@ -35,32 +32,51 @@ func newTaskRunCollector(taskName string, interfaceType InterfaceType, logger *s } } -func (c *taskRunCollector) log(level string, message string) { - action := normalizeAction(message) - result := "" +func (c *taskRunCollector) appendLog(entry models.TaskLogEntry) { + entry.Timestamp = time.Now() + + c.run.Logs = append(c.run.Logs, entry) + + if c.logger == nil { + return + } + + attrs := []any{ + "task", c.run.TaskName, + "interface", c.run.InterfaceType, + "action", entry.Action, + "result", entry.Result, + } + + switch entry.Level { + case "error": + c.logger.Error(entry.Message, attrs...) + case "warn": + c.logger.Warn(entry.Message, attrs...) + default: + c.logger.Info(entry.Message, attrs...) + } +} + +func (c *taskRunCollector) log(level, message string) { + result := "ok" switch level { case "error": result = "failed" case "warn": result = "warning" - default: - result = "ok" } c.mu.Lock() defer c.mu.Unlock() - c.run.Logs = append(c.run.Logs, models.TaskLogEntry{ - Timestamp: time.Now(), - Level: level, - Message: message, - Source: "system", - Action: action, - Result: result, + c.appendLog(models.TaskLogEntry{ + Level: level, + Message: message, + Source: "system", + Action: normalizeAction(message), + Result: result, }) - - attrs := []any{"action", action, "result", result, "task", c.run.TaskName, "interface", c.run.InterfaceType} - c.logWithLogger(level, message, attrs...) } func (c *taskRunCollector) recordUserAction(raw string) { @@ -69,42 +85,14 @@ func (c *taskRunCollector) recordUserAction(raw string) { c.mu.Lock() defer c.mu.Unlock() - c.run.Logs = append(c.run.Logs, models.TaskLogEntry{ - Timestamp: time.Now(), - Level: "user_action", - Message: raw, - Source: source, - Action: normalizeAction(actionText), - Target: target, - Result: result, + c.appendLog(models.TaskLogEntry{ + Level: "user_action", + Message: raw, + Source: source, + Action: normalizeAction(actionText), + Target: target, + Result: result, }) - - c.logWithLogger("info", "user action recorded", - "task", c.run.TaskName, - "interface", c.run.InterfaceType, - "source", source, - "action", normalizeAction(actionText), - "target", target, - "result", result, - ) -} - -func (c *taskRunCollector) setCompleted(completed bool) { - c.mu.Lock() - defer c.mu.Unlock() - - c.run.Completed = completed -} - -func (c *taskRunCollector) setError(err error) { - if err == nil { - return - } - - c.mu.Lock() - defer c.mu.Unlock() - - c.run.Error = err.Error() } func (c *taskRunCollector) recordQuestionnaire(completed bool, userQuit bool, answers map[string]any) { @@ -113,6 +101,7 @@ func (c *taskRunCollector) recordQuestionnaire(completed bool, userQuit bool, an c.run.QuestionnaireCompleted = completed c.run.QuestionnaireUserQuit = userQuit + if len(answers) > 0 { c.run.QuestionnaireAnswers = answers } @@ -124,128 +113,123 @@ func (c *taskRunCollector) recordQuestionnaire(completed bool, userQuit bool, an result = "incomplete" } - c.run.Logs = append(c.run.Logs, models.TaskLogEntry{ - Timestamp: time.Now(), - Level: "questionnaire", - Message: "questionnaire finished", - Source: "system", - Action: "questionnaire_finish", - Result: result, + c.appendLog(models.TaskLogEntry{ + Level: "questionnaire", + Message: "questionnaire finished", + Source: "system", + Action: "questionnaire_finish", + Result: result, }) - if len(answers) > 0 { - keys := make([]string, 0, len(answers)) - for key := range answers { - keys = append(keys, key) - } - sort.Strings(keys) - - for _, key := range keys { - value := answers[key] - if value == nil { - continue - } - - valueText := fmt.Sprintf("%v", value) - c.run.Logs = append(c.run.Logs, models.TaskLogEntry{ - Timestamp: time.Now(), - Level: "questionnaire", - Message: fmt.Sprintf("questionnaire answer: %s=%s", key, valueText), - Source: "system", - Action: "questionnaire_answer", - Target: key, - Result: valueText, - }) - } + if len(answers) == 0 { + return } - c.logWithLogger("info", "questionnaire recorded", - "task", c.run.TaskName, - "completed", completed, - "user_quit", userQuit, - "answers_count", len(answers), - ) + keys := make([]string, 0, len(answers)) + for k := range answers { + keys = append(keys, k) + } + sort.Strings(keys) + + for _, k := range keys { + v := answers[k] + if v == nil { + continue + } + + valueText := fmt.Sprintf("%v", v) + + c.appendLog(models.TaskLogEntry{ + Level: "questionnaire", + Message: fmt.Sprintf("questionnaire answer: %s=%s", k, valueText), + Source: "system", + Action: "questionnaire_answer", + Target: k, + Result: valueText, + }) + } } func (c *taskRunCollector) recordValidation(feedback ValidationFeedback) { c.mu.Lock() - defer c.mu.Unlock() c.run.ValidationAttempts++ attempt := c.run.ValidationAttempts + + result := "failed" if feedback.Success { + result = "passed" c.run.ValidationSuccesses++ } else { c.run.ValidationFailures++ } - c.run.Logs = append(c.run.Logs, models.TaskLogEntry{ - Timestamp: time.Now(), - Level: "validation", - Message: fmt.Sprintf("validation attempt %d success=%t", attempt, feedback.Success), - Source: "system", - Action: "validate_attempt", - Result: validationResult(feedback.Success), - Attempt: attempt, + c.appendLog(models.TaskLogEntry{ + Level: "validation", + Message: fmt.Sprintf("validation attempt %d", attempt), + Source: "system", + Action: "validate_attempt", + Result: result, + Attempt: attempt, }) - c.logWithLogger("info", "validation attempt", - "task", c.run.TaskName, - "interface", c.run.InterfaceType, - "attempt", attempt, - "result", validationResult(feedback.Success), - ) for _, check := range feedback.Checks { + checkResult := "failed" if check.Valid { + checkResult = "passed" c.run.ValidationChecksPassed++ - c.run.Logs = append(c.run.Logs, models.TaskLogEntry{ - Timestamp: time.Now(), - Level: "validation", - Message: fmt.Sprintf("validation check passed: %s", check.Message), - Source: "system", - Action: "validate_check", - Target: check.Message, - Result: "passed", - Attempt: attempt, - }) - c.logWithLogger("info", "validation check", - "task", c.run.TaskName, - "attempt", attempt, - "result", "passed", - "target", check.Message, - ) - continue + } else { + c.run.ValidationChecksFailed++ } - c.run.ValidationChecksFailed++ - c.run.Logs = append(c.run.Logs, models.TaskLogEntry{ - Timestamp: time.Now(), - Level: "validation", - Message: fmt.Sprintf("validation check failed: %s", check.Message), - Source: "system", - Action: "validate_check", - Target: check.Message, - Result: "failed", - Attempt: attempt, + + c.appendLog(models.TaskLogEntry{ + Level: "validation", + Message: fmt.Sprintf("validation check: %s", check.Message), + Source: "system", + Action: "validate_check", + Target: check.Message, + Result: checkResult, + Attempt: attempt, }) - c.logWithLogger("warn", "validation check", - "task", c.run.TaskName, - "attempt", attempt, - "result", "failed", - "target", check.Message, - ) } c.run.LastValidationMessage = feedback.Message + c.mu.Unlock() } -func validationResult(success bool) string { - if success { - return "passed" +func (c *taskRunCollector) setCompleted(completed bool) { + c.mu.Lock() + c.run.Completed = completed + c.mu.Unlock() +} + +func (c *taskRunCollector) setError(err error) { + if err == nil { + return } - return "failed" + c.mu.Lock() + c.run.Error = err.Error() + c.mu.Unlock() } -func normalizeUserAction(raw string) (source string, actionText string, target string, result string) { +func (c *taskRunCollector) finalize() models.TaskRunMetrics { + c.mu.Lock() + defer c.mu.Unlock() + + if c.run.EndedAt.IsZero() { + c.run.EndedAt = time.Now() + } + + c.run.DurationMs = + c.run.EndedAt.Sub(c.run.StartedAt).Milliseconds() + + final := c.run + final.Logs = append([]models.TaskLogEntry(nil), c.run.Logs...) + return final +} + +func normalizeUserAction(raw string) (source, actionText, target, result string) { + trimmed := strings.TrimSpace(raw) if trimmed == "" { return "unknown", "unknown_action", "", "unknown" @@ -256,57 +240,37 @@ func normalizeUserAction(raw string) (source string, actionText string, target s if source == "" { source = "unknown" } - actionText = strings.TrimSpace(event.Action) - if actionText == "" { - actionText = "unknown_action" - } - actionText = stripSourcePrefix(actionText, source) + + actionText = stripSourcePrefix( + strings.TrimSpace(event.Action), + source, + ) + target = strings.TrimSpace(event.Target) result = strings.TrimSpace(event.Result) + if result == "" { result = inferActionResult(actionText) } - return source, actionText, target, result + return } - result = "ok" lower := strings.ToLower(trimmed) if strings.HasPrefix(lower, "web request:") { rest := strings.TrimSpace(trimmed[len("web request:"):]) - parts := strings.Fields(rest) - if len(parts) >= 2 { - return "web", "request", strings.ToUpper(parts[0]) + " " + parts[1], "ok" - } return "web", "request", rest, "ok" } if strings.HasPrefix(lower, "repl command:") { - command := strings.TrimSpace(trimmed[len("repl command:"):]) - return "repl", "run_command", command, "ok" + cmd := strings.TrimSpace(trimmed[len("repl command:"):]) + return "repl", "run_command", cmd, "ok" } - words := strings.Fields(trimmed) - if len(words) > 1 { - head := strings.ToLower(words[0]) - if head == "tui" || head == "web" || head == "repl" { - source = head - actionText = strings.Join(words[1:], " ") - } else { - source = "unknown" - actionText = trimmed - } - } else { - source = "unknown" - actionText = trimmed - } - - result = inferActionResult(actionText) - - return source, actionText, "", result + return "unknown", trimmed, "", inferActionResult(trimmed) } -func stripSourcePrefix(actionText string, source string) string { +func stripSourcePrefix(actionText, source string) string { actionText = strings.TrimSpace(actionText) if actionText == "" || source == "" { return actionText @@ -327,22 +291,21 @@ func stripSourcePrefix(actionText string, source string) string { func inferActionResult(actionText string) string { lower := strings.ToLower(actionText) - if strings.Contains(lower, "failed") { + + switch { + case strings.Contains(lower, "failed"): return "failed" - } - if strings.Contains(lower, "canceled") { + case strings.Contains(lower, "canceled"): return "canceled" - } - if strings.Contains(lower, "started") { + case strings.Contains(lower, "started"): return "started" - } - if strings.Contains(lower, "submitted") { + case strings.Contains(lower, "submitted"): return "submitted" - } - if strings.Contains(lower, "requested") { + case strings.Contains(lower, "requested"): return "requested" + default: + return "ok" } - return "ok" } func normalizeAction(input string) string { @@ -350,153 +313,15 @@ func normalizeAction(input string) string { if lower == "" { return "unknown_action" } - clean := nonWord.ReplaceAllString(lower, "_") - clean = strings.Trim(clean, "_") + + clean := strings.Trim( + nonWord.ReplaceAllString(lower, "_"), + "_", + ) + if clean == "" { return "unknown_action" } + return clean } - -func (c *taskRunCollector) finalize() models.TaskRunMetrics { - c.mu.Lock() - defer c.mu.Unlock() - - if c.run.EndedAt.IsZero() { - c.run.EndedAt = time.Now() - } - c.run.DurationMs = c.run.EndedAt.Sub(c.run.StartedAt).Milliseconds() - - finalLogs := make([]models.TaskLogEntry, len(c.run.Logs)) - copy(finalLogs, c.run.Logs) - - final := c.run - final.Logs = finalLogs - - return final -} - -func appendTaskMetrics(path string, taskName string, run models.TaskRunMetrics, logger *slog.Logger) error { - if path == "" { - return nil - } - - dir := filepath.Dir(path) - if dir != "." { - if err := os.MkdirAll(dir, 0o755); err != nil { - return fmt.Errorf("failed to create metrics directory: %w", err) - } - } - - metrics := models.TaskMetricsFile{ - TaskName: taskName, - Runs: []models.TaskRunMetrics{}, - } - - if bytes, err := os.ReadFile(path); err == nil { - if len(bytes) > 0 { - if err := json.Unmarshal(bytes, &metrics); err != nil { - return fmt.Errorf("failed to parse metrics file %q: %w", path, err) - } - } - } else if !os.IsNotExist(err) { - return fmt.Errorf("failed to read metrics file %q: %w", path, err) - } - - if metrics.TaskName == "" { - metrics.TaskName = taskName - } - - run.RunID = len(metrics.Runs) + 1 - metrics.Runs = append(metrics.Runs, run) - metrics.Summary = buildTaskStatsSummary(metrics.Runs) - metrics.UpdatedAt = time.Now() - - data, err := json.MarshalIndent(metrics, "", " ") - if err != nil { - return fmt.Errorf("failed to encode task metrics: %w", err) - } - - if err := os.WriteFile(path, data, 0o644); err != nil { - return fmt.Errorf("failed to write metrics file %q: %w", path, err) - } - - if logger != nil { - logger.Info("task metrics persisted", - "path", path, - "task", taskName, - "run_id", run.RunID, - "total_runs", metrics.Summary.TotalRuns, - "completed_runs", metrics.Summary.CompletedRuns, - ) - } - - return nil -} - -func (c *taskRunCollector) logWithLogger(level string, message string, attrs ...any) { - if c.logger == nil { - return - } - - switch level { - case "error": - c.logger.Error(message, attrs...) - case "warn": - c.logger.Warn(message, attrs...) - default: - c.logger.Info(message, attrs...) - } -} - -func buildTaskStatsSummary(runs []models.TaskRunMetrics) models.TaskStatsSummary { - summary := models.TaskStatsSummary{} - if len(runs) == 0 { - return summary - } - - summary.TotalRuns = len(runs) - summary.FirstRunStartedAt = runs[0].StartedAt - - for _, run := range runs { - summary.TotalDurationMs += run.DurationMs - summary.ValidationAttempts += run.ValidationAttempts - summary.ValidationSuccesses += run.ValidationSuccesses - summary.ValidationFailures += run.ValidationFailures - summary.ValidationChecksPassed += run.ValidationChecksPassed - summary.ValidationChecksFailed += run.ValidationChecksFailed - - if run.Completed { - summary.CompletedRuns++ - } else { - summary.IncompleteRuns++ - } - - if run.QuestionnaireCompleted { - summary.QuestionnairesCompleted++ - } - if run.QuestionnaireUserQuit { - summary.QuestionnairesAbandoned++ - } - - if run.StartedAt.Before(summary.FirstRunStartedAt) { - summary.FirstRunStartedAt = run.StartedAt - } - if run.StartedAt.After(summary.LastRunStartedAt) { - summary.LastRunStartedAt = run.StartedAt - } - if run.EndedAt.After(summary.LastRunEndedAt) { - summary.LastRunEndedAt = run.EndedAt - summary.LastInterfaceType = run.InterfaceType - } - - for _, log := range run.Logs { - if log.Level == "user_action" { - summary.TotalUserActions++ - } - } - } - - summary.AverageDurationMs = summary.TotalDurationMs / int64(summary.TotalRuns) - return summary -} diff --git a/pkg/task/metrics_store.go b/pkg/task/metrics_store.go new file mode 100644 index 0000000..554e80f --- /dev/null +++ b/pkg/task/metrics_store.go @@ -0,0 +1,87 @@ +package task + +import ( + "context" + "encoding/json" + "fmt" + "log/slog" + "os" + "path/filepath" + "time" + + "github.com/LazyBachelor/LazyPM/internal/models" +) + +type MetricsStore interface { + Append(ctx context.Context, taskName string, run models.TaskRunMetrics) error +} + +type FileMetricsStore struct { + path string + logger *slog.Logger +} + +func NewFileMetricsStore(path string, logger *slog.Logger) *FileMetricsStore { + return &FileMetricsStore{ + path: path, + logger: logger, + } +} + +func (s *FileMetricsStore) Append(ctx context.Context, taskName string, run models.TaskRunMetrics) error { + + if s.path == "" { + return nil + } + + dir := filepath.Dir(s.path) + if dir != "." { + if err := os.MkdirAll(dir, 0o755); err != nil { + return fmt.Errorf("create metrics directory: %w", err) + } + } + + metrics := models.TaskMetricsFile{ + TaskName: taskName, + Runs: []models.TaskRunMetrics{}, + } + + if bytes, err := os.ReadFile(s.path); err == nil { + if len(bytes) > 0 { + if err := json.Unmarshal(bytes, &metrics); err != nil { + return fmt.Errorf("parse metrics file: %w", err) + } + } + } else if !os.IsNotExist(err) { + return fmt.Errorf("read metrics file: %w", err) + } + + if metrics.TaskName == "" { + metrics.TaskName = taskName + } + + run.RunID = len(metrics.Runs) + 1 + metrics.Runs = append(metrics.Runs, run) + metrics.Summary = buildTaskStatsSummary(metrics.Runs) + metrics.UpdatedAt = time.Now() + + data, err := json.MarshalIndent(metrics, "", " ") + if err != nil { + return fmt.Errorf("encode metrics: %w", err) + } + + if err := os.WriteFile(s.path, data, 0o644); err != nil { + return fmt.Errorf("write metrics file: %w", err) + } + + if s.logger != nil { + s.logger.Info( + "task metrics persisted", + "path", s.path, + "task", taskName, + "run_id", run.RunID, + ) + } + + return nil +} diff --git a/pkg/task/metrics_summary.go b/pkg/task/metrics_summary.go new file mode 100644 index 0000000..777d850 --- /dev/null +++ b/pkg/task/metrics_summary.go @@ -0,0 +1,72 @@ +package task + +import ( + "github.com/LazyBachelor/LazyPM/internal/models" +) + +func buildTaskStatsSummary(runs []models.TaskRunMetrics) models.TaskStatsSummary { + + var summary models.TaskStatsSummary + + if len(runs) == 0 { + return summary + } + + summary.TotalRuns = len(runs) + + for i, run := range runs { + + // First run handling + if i == 0 || run.StartedAt.Before(summary.FirstRunStartedAt) { + summary.FirstRunStartedAt = run.StartedAt + } + + if run.StartedAt.After(summary.LastRunStartedAt) { + summary.LastRunStartedAt = run.StartedAt + } + + if run.EndedAt.After(summary.LastRunEndedAt) { + summary.LastRunEndedAt = run.EndedAt + summary.LastInterfaceType = run.InterfaceType + } + + // Duration + summary.TotalDurationMs += run.DurationMs + + // Completion + if run.Completed { + summary.CompletedRuns++ + } else { + summary.IncompleteRuns++ + } + + // Validation + summary.ValidationAttempts += run.ValidationAttempts + summary.ValidationSuccesses += run.ValidationSuccesses + summary.ValidationFailures += run.ValidationFailures + summary.ValidationChecksPassed += run.ValidationChecksPassed + summary.ValidationChecksFailed += run.ValidationChecksFailed + + // Questionnaire + if run.QuestionnaireCompleted { + summary.QuestionnairesCompleted++ + } + if run.QuestionnaireUserQuit { + summary.QuestionnairesAbandoned++ + } + + // User actions + for _, log := range run.Logs { + if log.Level == "user_action" { + summary.TotalUserActions++ + } + } + } + + if summary.TotalRuns > 0 { + summary.AverageDurationMs = + summary.TotalDurationMs / int64(summary.TotalRuns) + } + + return summary +} diff --git a/pkg/task/runner.go b/pkg/task/runner.go index 9642366..4685a5f 100644 --- a/pkg/task/runner.go +++ b/pkg/task/runner.go @@ -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 } diff --git a/pkg/task/validation.go b/pkg/task/validation.go new file mode 100644 index 0000000..170c670 --- /dev/null +++ b/pkg/task/validation.go @@ -0,0 +1,45 @@ +package task + +import ( + "context" + "time" +) + +type ValidationEngine struct { + task Tasker +} + +func (v *ValidationEngine) Start(ctx context.Context, onFeedback func(ValidationFeedback)) (done <-chan struct{}, stop chan<- struct{}) { + doneChan := make(chan struct{}, 1) + stopChan := make(chan struct{}, 1) + + go func() { + ticker := time.NewTicker(time.Second) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + feedback := v.task.Validate(ctx) + + if onFeedback != nil { + onFeedback(feedback) + } + + if feedback.Success { + time.Sleep(3 * time.Second) + doneChan <- struct{}{} + return + } + + case <-stopChan: + return + + case <-ctx.Done(): + return + } + } + }() + + return doneChan, stopChan +} From 0cb45d811d9fd431aa3030ad065966980c8db25c Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Tue, 3 Mar 2026 11:52:34 +0100 Subject: [PATCH 16/51] remove button clicks as we have user actions --- internal/app/statistics.go | 1 - internal/models/statistics.go | 5 ----- 2 files changed, 6 deletions(-) diff --git a/internal/app/statistics.go b/internal/app/statistics.go index fce991d..dadc342 100644 --- a/internal/app/statistics.go +++ b/internal/app/statistics.go @@ -94,7 +94,6 @@ func (s *StatisticsService) RecordTaskRun(ctx context.Context, run models.TaskRu } } stats.TotalUserActions += userActions - stats.ButtonClicks.Clicks += userActions if run.QuestionnaireCompleted { stats.QuestionnairesCompleted++ } diff --git a/internal/models/statistics.go b/internal/models/statistics.go index 6581084..b151072 100644 --- a/internal/models/statistics.go +++ b/internal/models/statistics.go @@ -30,11 +30,6 @@ type Statistics struct { LastTaskName string `json:"last_task_name"` LastRunID int `json:"last_run_id"` - ButtonClicks ButtonClicks `json:"button_clicks"` -} - -type ButtonClicks struct { - Clicks int `json:"clicks"` } type TaskMetricsFile struct { From bbf5e1f7a92d5aaa296ee1aa825d191686eca9ce Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Tue, 3 Mar 2026 11:56:56 +0100 Subject: [PATCH 17/51] use web logger only if dev --- pkg/web/server/routes.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pkg/web/server/routes.go b/pkg/web/server/routes.go index 5ed3637..236d917 100644 --- a/pkg/web/server/routes.go +++ b/pkg/web/server/routes.go @@ -20,7 +20,9 @@ import ( func (s *Server) RegisterRoutes(assets embed.FS) http.Handler { r := chi.NewRouter() - r.Use(middleware.Logger) + if os.Getenv("DEV") == "True" { + r.Use(middleware.Logger) + } r.Use(middleware.Recoverer) r.Use(cors.AllowAll().Handler) r.Use(middleware.CleanPath) From eafba7188739cb1860deb331f6a292de2149af4d Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Tue, 3 Mar 2026 13:39:09 +0100 Subject: [PATCH 18/51] make more readable --- pkg/task/metrics_store.go | 35 +++++++++++++++++++---------------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/pkg/task/metrics_store.go b/pkg/task/metrics_store.go index 554e80f..b591058 100644 --- a/pkg/task/metrics_store.go +++ b/pkg/task/metrics_store.go @@ -29,7 +29,6 @@ func NewFileMetricsStore(path string, logger *slog.Logger) *FileMetricsStore { } func (s *FileMetricsStore) Append(ctx context.Context, taskName string, run models.TaskRunMetrics) error { - if s.path == "" { return nil } @@ -46,14 +45,8 @@ func (s *FileMetricsStore) Append(ctx context.Context, taskName string, run mode Runs: []models.TaskRunMetrics{}, } - if bytes, err := os.ReadFile(s.path); err == nil { - if len(bytes) > 0 { - if err := json.Unmarshal(bytes, &metrics); err != nil { - return fmt.Errorf("parse metrics file: %w", err) - } - } - } else if !os.IsNotExist(err) { - return fmt.Errorf("read metrics file: %w", err) + if err := readMetrics(&metrics, s.path); err != nil { + return err } if metrics.TaskName == "" { @@ -74,14 +67,24 @@ func (s *FileMetricsStore) Append(ctx context.Context, taskName string, run mode return fmt.Errorf("write metrics file: %w", err) } - if s.logger != nil { - s.logger.Info( - "task metrics persisted", - "path", s.path, - "task", taskName, - "run_id", run.RunID, - ) + if s.logger == nil { + return fmt.Errorf("logger is nil") } + s.logger.Info("task metrics persisted", "path", s.path, "task", taskName, "run_id", run.RunID) + + return nil +} + +func readMetrics(metrics *models.TaskMetricsFile, path string) error { + if bytes, err := os.ReadFile(path); err == nil { + if len(bytes) > 0 { + if err := json.Unmarshal(bytes, metrics); err != nil { + return fmt.Errorf("failed to parse metrics file: %w", err) + } + } + } else if !os.IsNotExist(err) { + return fmt.Errorf("failed to read metrics file: %w", err) + } return nil } From 4a6b40509c5a484d002e18c770f86b940086d9c0 Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Tue, 3 Mar 2026 13:39:20 +0100 Subject: [PATCH 19/51] dont need whitespace here --- pkg/task/metrics.go | 1 - pkg/task/metrics_summary.go | 1 - 2 files changed, 2 deletions(-) diff --git a/pkg/task/metrics.go b/pkg/task/metrics.go index 19990c9..ce573e1 100644 --- a/pkg/task/metrics.go +++ b/pkg/task/metrics.go @@ -229,7 +229,6 @@ func (c *taskRunCollector) finalize() models.TaskRunMetrics { } func normalizeUserAction(raw string) (source, actionText, target, result string) { - trimmed := strings.TrimSpace(raw) if trimmed == "" { return "unknown", "unknown_action", "", "unknown" diff --git a/pkg/task/metrics_summary.go b/pkg/task/metrics_summary.go index 777d850..3a847d2 100644 --- a/pkg/task/metrics_summary.go +++ b/pkg/task/metrics_summary.go @@ -5,7 +5,6 @@ import ( ) func buildTaskStatsSummary(runs []models.TaskRunMetrics) models.TaskStatsSummary { - var summary models.TaskStatsSummary if len(runs) == 0 { From 0b5d94b2eec29dfc0bb0b1d4f8a2a3f1eb50c166 Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Tue, 3 Mar 2026 14:07:03 +0100 Subject: [PATCH 20/51] fix typo --- pkg/task/{lifecsycle.go => lifecycle.go} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename pkg/task/{lifecsycle.go => lifecycle.go} (100%) diff --git a/pkg/task/lifecsycle.go b/pkg/task/lifecycle.go similarity index 100% rename from pkg/task/lifecsycle.go rename to pkg/task/lifecycle.go From 4bb4dac4fd2c2a04a7d80f69939f5dc4923b6b55 Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Wed, 4 Mar 2026 13:24:48 +0100 Subject: [PATCH 21/51] add mongodb for submitting metrics and stats --- go.mod | 9 +++ go.sum | 42 +++++++++++ internal/commands/survey/submit.go | 115 ++++++++++++++++++++++++++++- 3 files changed, 164 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index d87e220..54094f7 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,8 @@ require ( github.com/go-git/go-git/v6 v6.0.0-20260222090600-424e9964d3a3 github.com/muesli/reflow v0.3.0 github.com/steveyegge/beads v0.49.6 + go.mongodb.org/mongo-driver v1.17.9 + go.mongodb.org/mongo-driver/v2 v2.5.0 ) // Terminal dependencies @@ -67,9 +69,11 @@ require ( github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-viper/mapstructure/v2 v2.5.0 // indirect github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect + github.com/golang/snappy v0.0.4 // indirect github.com/haatos/goshipit v0.0.0-20260206030541-056850f43320 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/kevinburke/ssh_config v1.5.0 // indirect + github.com/klauspost/compress v1.18.0 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect github.com/lucasb-eyer/go-colorful v1.3.0 // indirect @@ -79,6 +83,7 @@ require ( github.com/mattn/go-runewidth v0.0.19 // indirect github.com/mattn/go-tty v0.0.7 // indirect github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect + github.com/montanaflynn/stats v0.7.1 // indirect github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/mango v0.2.0 // indirect @@ -102,7 +107,11 @@ require ( github.com/spf13/viper v1.21.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/tetratelabs/wazero v1.11.0 // indirect + github.com/xdg-go/pbkdf2 v1.0.0 // indirect + github.com/xdg-go/scram v1.2.0 // indirect + github.com/xdg-go/stringprep v1.0.4 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect + github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/crypto v0.48.0 // indirect golang.org/x/exp v0.0.0-20260209203927-2842357ff358 // indirect diff --git a/go.sum b/go.sum index 30fb654..8bdd730 100644 --- a/go.sum +++ b/go.sum @@ -124,6 +124,8 @@ github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPE github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/haatos/goshipit v0.0.0-20260206030541-056850f43320 h1:sb7SfxZfN+U9OHC61tcS98Ge0zY9uEkW5CP6KB4YVHg= @@ -132,6 +134,8 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/kevinburke/ssh_config v1.5.0 h1:3cPZmE54xb5j3G5xQCjSvokqNwU2uW+3ry1+PRLSPpA= github.com/kevinburke/ssh_config v1.5.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= @@ -168,6 +172,8 @@ github.com/mattn/go-tty v0.0.7 h1:KJ486B6qI8+wBO7kQxYgmmEFDaFEE96JMBQ7h400N8Q= github.com/mattn/go-tty v0.0.7/go.mod h1:f2i5ZOvXBU/tCABmLmOfzLz9azMo5wdAaElRNnJKr+k= github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4= github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE= +github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE= +github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= @@ -235,23 +241,45 @@ github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8 github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/tetratelabs/wazero v1.11.0 h1:+gKemEuKCTevU4d7ZTzlsvgd1uaToIDtlQlmNbwqYhA= github.com/tetratelabs/wazero v1.11.0/go.mod h1:eV28rsN8Q+xwjogd7f4/Pp4xFxO7uOGbLcD/LzB1wiU= +github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= +github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= +github.com/xdg-go/scram v1.2.0 h1:bYKF2AEwG5rqd1BumT4gAnvwU/M9nBp2pTSxeZw7Wvs= +github.com/xdg-go/scram v1.2.0/go.mod h1:3dlrS0iBaWKYVt2ZfA4cj48umJZ+cAEbR6/SjLA88I8= +github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8= +github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= +github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM= +github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.mongodb.org/mongo-driver v1.17.9 h1:IexDdCuuNJ3BHrELgBlyaH9p60JXAvdzWR128q+U5tU= +go.mongodb.org/mongo-driver v1.17.9/go.mod h1:LlOhpH5NUEfhxcAwG0UEkMqwYcc4JU18gtCdGudk/tQ= +go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE= +go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= golang.org/x/exp v0.0.0-20260209203927-2842357ff358 h1:kpfSV7uLwKJbFSEgNhWzGSL47NDSF/5pYYQw1V0ub6c= golang.org/x/exp v0.0.0-20260209203927-2842357ff358/go.mod h1:R3t0oliuryB5eenPWl3rrQxwnNM3WTwnsRZZiXLAAW8= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60= golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -259,16 +287,30 @@ golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200918174421-af09f7315aff/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg= golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= diff --git a/internal/commands/survey/submit.go b/internal/commands/survey/submit.go index 205d93e..8d0a7b2 100644 --- a/internal/commands/survey/submit.go +++ b/internal/commands/survey/submit.go @@ -1,12 +1,123 @@ package survey -import "github.com/spf13/cobra" +import ( + "encoding/json" + "fmt" + "os" + "strings" + + "github.com/LazyBachelor/LazyPM/internal/models" + "github.com/spf13/cobra" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" +) + +var mongoURI = os.Getenv("MONGODB_URI") var SubmitCmd = &cobra.Command{ Use: "submit", Short: "Submit your survey responses", RunE: func(cmd *cobra.Command, args []string) error { - cmd.Println("Submitting responses and metrics...") + client, error := mongo.Connect(cmd.Context(), options.Client().ApplyURI(mongoURI)) + if error != nil { + return fmt.Errorf("Failed to connect to MongoDB: %v", error) + } + + go func() { + if err := client.Disconnect(cmd.Context()); err != nil { + fmt.Printf("Failed to disconnect MongoDB client: %v", err) + } + }() + + userStatscollection := client.Database("Responses").Collection("stats") + taskMetricsCollection := client.Database("Responses").Collection("task_metrics") + + pmDir := "./.pm/" + + entries, err := os.ReadDir(pmDir) + if err != nil { + return fmt.Errorf("Failed to read .pm directory: %v", err) + } + + if len(entries) == 0 { + return fmt.Errorf("No files found in .pm directory") + } + + statFile := pmDir + "stats.json" + if _, err := os.Stat(statFile); os.IsNotExist(err) { + return fmt.Errorf("stats.json not found in .pm directory") + } + + stats, err := getStats(statFile) + if err != nil { + return fmt.Errorf("Failed to read stats.json: %v", err) + } + + _, err = userStatscollection.InsertOne(cmd.Context(), stats) + if err != nil { + return fmt.Errorf("Failed to insert stats into database: %v", err) + } + + metricFiles := []string{} + for _, entry := range entries { + if entry.IsDir() { + continue + } + + if entry.Name() == "stats.json" { + continue + } + + if strings.HasSuffix(entry.Name(), "-stats.json") { + metricFiles = append(metricFiles, pmDir+entry.Name()) + continue + } + } + + for _, file := range metricFiles { + metrics, err := getTaskMetrics(file) + if err != nil { + fmt.Printf("failed to read metrics from %s: %v", file, err) + continue + } + + _, err = taskMetricsCollection.InsertOne(cmd.Context(), metrics) + if err != nil { + fmt.Printf("failed to insert metrics from %s: %v", file, err) + continue + } + } + + cmd.Printf("Successfully submitted survey responses and metrics to the database") + return nil }, } + +func getStats(file string) (*models.Statistics, error) { + data, err := os.ReadFile(file) + if err != nil { + return nil, err + } + + var stats models.Statistics + if err := json.Unmarshal(data, &stats); err != nil { + return nil, err + } + + return &stats, nil +} + +func getTaskMetrics(file string) (*models.TaskMetricsFile, error) { + data, err := os.ReadFile(file) + if err != nil { + return nil, err + } + + var metrics models.TaskMetricsFile + if err := json.Unmarshal(data, &metrics); err != nil { + return nil, err + } + + return &metrics, nil +} From 77b38692c695d289d66117eeb7ea488c686135e8 Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Wed, 4 Mar 2026 14:00:35 +0100 Subject: [PATCH 22/51] align task with description add a complete with message in case we need that --- cmd/pm/tasks/createIssue.go | 38 +++++++++++++++++++++++++++------- internal/utils/check/expect.go | 8 +++++++ pkg/task/runner.go | 4 +++- 3 files changed, 41 insertions(+), 9 deletions(-) diff --git a/cmd/pm/tasks/createIssue.go b/cmd/pm/tasks/createIssue.go index b10c32c..952b322 100644 --- a/cmd/pm/tasks/createIssue.go +++ b/cmd/pm/tasks/createIssue.go @@ -4,6 +4,7 @@ import ( "context" "fmt" + "github.com/LazyBachelor/LazyPM/internal/models" "github.com/LazyBachelor/LazyPM/internal/utils/check" ) @@ -12,10 +13,10 @@ const description = `You are tasked with creating a new issue in the project man This task will test your ability to use the issue creation workflow effectively. Your task: -1. Create a new issue with a clear title -2. Add a detailed description explaining what needs to be done -3. Assign the issue to yourself -4. Mark the issue as in-progress when you start working on it +1. Create a new issue with a the title "My first Issue" +2. Add this detailed description "I need to do some coding" +3. Assign the issue to yourself as "Me" +4. Mark the issue as In Progress when you start working on it 5. Close the issue once you've completed the work Make sure to fill out all the necessary details to help others understand the work item.` @@ -55,6 +56,8 @@ func (t *CreateIssueTask) Setup(ctx context.Context) error { return t.app.Issues.CreateIssue(ctx, t.setupIssue, "") } +var isInProgress = false + func (t *CreateIssueTask) Validate(ctx context.Context) ValidationFeedback { expect := check.NewExpector() @@ -63,9 +66,6 @@ func (t *CreateIssueTask) Validate(ctx context.Context) ValidationFeedback { return expect.ValidationFeedback } - expect.NotEmptyString(t.setupIssue.Assignee, - fmt.Sprintf("%s is not assigned to anyone", t.setupIssue.ID)) - if len(issues) == 0 { expect.Fail("No new issues created") return expect.ValidationFeedback @@ -73,8 +73,30 @@ func (t *CreateIssueTask) Validate(ctx context.Context) ValidationFeedback { issue := issues[0] - expect.Assert(len(issues) < 2, "Multiple issues were created instead of one") + expect.Assert(len(issues) < 2, "Multiple issues were created instead of one delete the extra issues and try again") + + expect.NotEmptyString(issue.Title, "Issue title should not be empty") + expect.Assert(issue.Title == "My first Issue", + fmt.Sprintf("Issue title does not match the expected value 'My first Issue', but was '%s'", issue.Title)) + expect.NotEmptyString(issue.Description, "Issue description should not be empty") + expect.Assert(issue.Description == "I need to do some coding", + fmt.Sprintf("Issue description does not match the expected value 'I need to do some coding', but was '%s'", issue.Description)) + + expect.Assert(issue.Assignee == "Me", + fmt.Sprintf("Issue should be assigned to 'Me', but was assigned to '%s'", issue.Assignee)) + + if issue.Status == models.StatusInProgress || isInProgress { + isInProgress = true + } else { + expect.Fail("Issue should be marked as in-progress when work starts") + } + + if !isInProgress { + return expect.ValidationFeedback + } else if issue.Status != models.StatusClosed { + expect.Fail("Issue should be set to Closed once the work is completed") + } return expect.Complete() } diff --git a/internal/utils/check/expect.go b/internal/utils/check/expect.go index cfa40a2..670fcdb 100644 --- a/internal/utils/check/expect.go +++ b/internal/utils/check/expect.go @@ -35,6 +35,14 @@ func (e *Expector) Complete() ValidationFeedback { return e.ValidationFeedback } +func (e *Expector) CompleteWithMessage(message string) ValidationFeedback { + e.Success = len(e.Errors()) == 0 + if !e.Success { + e.Message = message + } + return e.ValidationFeedback +} + func (e *Expector) Fail(message string) ValidationFeedback { e.Checks = append(e.Checks, NewCheck(message, false)) return e.ValidationFeedback diff --git a/pkg/task/runner.go b/pkg/task/runner.go index a9c4389..ea15b2b 100644 --- a/pkg/task/runner.go +++ b/pkg/task/runner.go @@ -100,7 +100,9 @@ func startValidationLoop(ctx context.Context, t Tasker, feedbackChan chan Valida case <-ticker.C: feedback := t.Validate(ctx) if feedback.Success { - feedback.Message = "Task completed successfully!" + if feedback.Message == "" { + feedback.Message = "Task completed successfully! Going back to the survey menu..." + } feedbackChan <- feedback time.Sleep(4 * time.Second) doneChan <- true From 2fcd312072f055014a4d524e549b222f79ecf283 Mon Sep 17 00:00:00 2001 From: Robin Olsen <129996395+Telikz@users.noreply.github.com> Date: Wed, 4 Mar 2026 14:02:48 +0100 Subject: [PATCH 23/51] Update pkg/task/metrics_store.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- pkg/task/metrics_store.go | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/pkg/task/metrics_store.go b/pkg/task/metrics_store.go index b591058..ae69f25 100644 --- a/pkg/task/metrics_store.go +++ b/pkg/task/metrics_store.go @@ -67,12 +67,9 @@ func (s *FileMetricsStore) Append(ctx context.Context, taskName string, run mode return fmt.Errorf("write metrics file: %w", err) } - if s.logger == nil { - return fmt.Errorf("logger is nil") + if s.logger != nil { + s.logger.Info("task metrics persisted", "path", s.path, "task", taskName, "run_id", run.RunID) } - - s.logger.Info("task metrics persisted", "path", s.path, "task", taskName, "run_id", run.RunID) - return nil } From 81a046e9358c3a8da937c01ab9a602fe9d50dca4 Mon Sep 17 00:00:00 2001 From: Robin Olsen <129996395+Telikz@users.noreply.github.com> Date: Wed, 4 Mar 2026 14:20:47 +0100 Subject: [PATCH 24/51] Update cmd/pm/tasks/createIssue.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- cmd/pm/tasks/createIssue.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/pm/tasks/createIssue.go b/cmd/pm/tasks/createIssue.go index 952b322..21cef28 100644 --- a/cmd/pm/tasks/createIssue.go +++ b/cmd/pm/tasks/createIssue.go @@ -13,7 +13,7 @@ const description = `You are tasked with creating a new issue in the project man This task will test your ability to use the issue creation workflow effectively. Your task: -1. Create a new issue with a the title "My first Issue" +1. Create a new issue with the title "My first Issue" 2. Add this detailed description "I need to do some coding" 3. Assign the issue to yourself as "Me" 4. Mark the issue as In Progress when you start working on it From a47ff2a39b71d5a01af9286b7681010b76d6915d Mon Sep 17 00:00:00 2001 From: Robin Olsen <129996395+Telikz@users.noreply.github.com> Date: Wed, 4 Mar 2026 14:21:13 +0100 Subject: [PATCH 25/51] Update cmd/pm/tasks/createIssue.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- cmd/pm/tasks/createIssue.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/pm/tasks/createIssue.go b/cmd/pm/tasks/createIssue.go index 21cef28..c6757d4 100644 --- a/cmd/pm/tasks/createIssue.go +++ b/cmd/pm/tasks/createIssue.go @@ -73,7 +73,7 @@ func (t *CreateIssueTask) Validate(ctx context.Context) ValidationFeedback { issue := issues[0] - expect.Assert(len(issues) < 2, "Multiple issues were created instead of one delete the extra issues and try again") + expect.Assert(len(issues) < 2, "Multiple issues were created instead of one. Delete the extra issues and try again.") expect.NotEmptyString(issue.Title, "Issue title should not be empty") expect.Assert(issue.Title == "My first Issue", From c3bbcfd83232fd4b550089d299c1e16cabffcbab Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Wed, 4 Mar 2026 15:14:45 +0100 Subject: [PATCH 26/51] update coding task to alignt with latest iteration --- cmd/pm/tasks/codingTask.go | 181 ++++++++++++++++++++++++++++--------- 1 file changed, 137 insertions(+), 44 deletions(-) diff --git a/cmd/pm/tasks/codingTask.go b/cmd/pm/tasks/codingTask.go index 5b7d3e0..c06cb22 100644 --- a/cmd/pm/tasks/codingTask.go +++ b/cmd/pm/tasks/codingTask.go @@ -2,37 +2,80 @@ package tasks import ( "context" + "fmt" "os" + "strings" + "github.com/LazyBachelor/LazyPM/internal/models" "github.com/LazyBachelor/LazyPM/internal/utils/check" - "github.com/charmbracelet/huh" ) -const codingDescription = `You are tasked with writing a simple function. +const codingDescription = `You are tasked with doing a chore in the codebase. -This task will test your ability to write clean, working code. +This task will test your ablity to read and understand instructions, change text, and save it to a file. + +The MongoDB Driver dependency in the file is outdated and needs to be updated to the latest version. +This is a common task for developers, and it requires attention to detail and the ability to follow instructions carefully. Your task: -1. Review the requirements below -2. Write a function that takes two integers and returns their sum -3. The function should be named "Add" -4. The function should be part of the "coding" package -5. Save your code to the code.txt file +1. Assing this Issue to yourself as "Me" and mark it as "In Progress" +2. Create a New Issue and give it these details: + - Title: "Upgrade MongoDB Driver Dependency" + - Description: "We need to upgrade the MongoDB Driver dependency to the latest version." + - Status: "In Progress" + - Issue Type: "Chore" +3. A file will appear on your desktop named "code.txt". + Open it and follow the instructions inside. And save the file after you are done. +4. When you are done, mark this and the issue you made as "Closed".` -Requirements: -- Function name: Add -- Parameters: two integers -- Return value: integer (sum of the two inputs) -- Package: coding` +var textFileDescription = ` +Please upgrade the MongoDB Driver dependency in the go.mod file to the latest version. +It should be v1.17.9. After you are done, save the file and mark the task as completed. +############################################################` -var textFileContent = codingDescription + ` -Please write your code below this line! -############################################################ +var code = ` +require ( + charm.land/lipgloss/v2 v2.0.0-beta.3.0.20251106193318-19329a3e8410 + github.com/go-git/go-git/v6 v6.0.0-20260222090600-424e9964d3a3 + github.com/muesli/reflow v0.3.0 + github.com/steveyegge/beads v0.49.6 + go.mongodb.org/mongo-driver v1.17.8 + go.mongodb.org/mongo-driver/v2 v2.5.0 +) + +require ( + github.com/c-bata/go-prompt v0.2.6 + github.com/charmbracelet/bubbles v0.21.1 + github.com/charmbracelet/bubbletea v1.3.10 + github.com/charmbracelet/fang v0.4.4 + github.com/charmbracelet/huh v0.8.0 + github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 + github.com/spf13/cobra v1.10.2 + golang.org/x/term v0.40.0 +) + +require ( + github.com/NYTimes/gziphandler v1.1.1 + github.com/a-h/templ v0.3.977 + github.com/donseba/go-htmx v1.12.1 + github.com/go-chi/chi/v5 v5.2.5 + github.com/go-playground/form/v4 v4.3.0 + github.com/go-playground/validator/v10 v10.30.1 + github.com/rs/cors v1.11.1 +) + +tool ( + github.com/a-h/templ/cmd/templ + github.com/haatos/goshipit/cmd/gsi +) ` +var textFileContent = codingDescription + textFileDescription + "\n" + code + type CodingTask struct { - done bool - app *App + done bool + setupIssue *Issue + app *App } func NewCodingTask(app *App) *CodingTask { @@ -48,35 +91,11 @@ func (t *CodingTask) Details() TaskDetails { } func (t *CodingTask) Questions(interfaceType InterfaceType) (questions Questions) { - return BaseQuestions(interfaceType). - With( - ReplQuestion(interfaceType, - huh.NewConfirm().Key("repl_experience").Title("Question only for REPL interface")), - ). - With( - WebQuestion(interfaceType, - huh.NewInput().Key("web_feedback").Title("Question only for Web interface")), - ). - With( - TUIQuestion(interfaceType, - huh.NewConfirm().Key("tui_experience").Title("Question only for TUI interface")), - ). - With( - Question( - huh.NewConfirm().Key("final_confirmation").Title("One last question for all interfaces!")), - ) + return BaseQuestions(interfaceType) } func (t *CodingTask) QuestionnaireKeys(interfaceType InterfaceType) []string { keys := []string{"task_completed", "task_difficulty", "final_confirmation"} - switch interfaceType { - case InterfaceTypeREPL: - keys = append(keys, "repl_experience") - case InterfaceTypeWeb: - keys = append(keys, "web_feedback") - case InterfaceTypeTUI: - keys = append(keys, "tui_experience") - } return keys } @@ -85,6 +104,17 @@ func (t *CodingTask) Setup(ctx context.Context) error { return err } + t.setupIssue = NewIssueBuilder(). + WithTitle("Coding Task - Upgrade MongoDB Driver"). + WithDescription(codingDescription). + WithStatus(models.StatusOpen). + WithIssueType(models.TypeTask). + Build() + + if err := t.app.Issues.CreateIssue(ctx, t.setupIssue, "LazyPM"); err != nil { + return err + } + if err := os.WriteFile("./code.txt", []byte(textFileContent), 0644); err != nil { return err } @@ -94,6 +124,69 @@ func (t *CodingTask) Setup(ctx context.Context) error { func (t *CodingTask) Validate(ctx context.Context) ValidationFeedback { expect := check.NewExpector() - expect.Assert(true, "This task is always valid") + + issues, err := FetchIssues(ctx, t.app, t.setupIssue) + if err != nil { + return expect.ValidationFeedback + } + + expect.Assert(t.setupIssue.Assignee == "Me", "The original issue should be assigned to 'Me'. Please assign the issue to yourself.") + expect.Assert(len(issues) > 0, "No new issues created. Please create an issue with the specified details.") + + if len(issues) == 0 { + expect.Fail("No new issues created") + return expect.ValidationFeedback + } + + issue := issues[0] + + expect.Assert(len(issues) < 2, "Multiple issues were created instead of one. Delete the extra issues and try again.") + + expect.NotEmptyString(issue.Title, "Issue title should not be empty") + expect.Assert(issue.Title == "Upgrade MongoDB Driver Dependency", + fmt.Sprintf("Issue title does not match the expected value 'Upgrade MongoDB Driver Dependency', but was '%s'", issue.Title)) + + expect.NotEmptyString(issue.Description, "Issue description should not be empty") + expect.Assert(issue.Description == "We need to upgrade the MongoDB Driver dependency to the latest version.", + fmt.Sprintf("Issue description does not match the expected value 'We need to upgrade the MongoDB Driver dependency to the latest version.', but was '%s'", issue.Description)) + + expect.Assert(issue.IssueType == models.TypeChore, + fmt.Sprintf("Issue type should be 'Chore', but was '%s'", issue.IssueType)) + + expect.Assert(issue.Assignee == "Me", + fmt.Sprintf("Issue should be assigned to 'Me', but was assigned to '%s'", issue.Assignee)) + + if issue.Status == models.StatusInProgress || isInProgress { + isInProgress = true + } else { + expect.Fail("Issue should be marked as in-progress when work starts") + } + + if _, err := os.Stat("./code.txt"); os.IsNotExist(err) { + expect.Fail("The code.txt file should exist on the desktop. Please create the file with the specified content.") + return expect.ValidationFeedback + } + + fileContent, err := os.ReadFile("./code.txt") + if err != nil { + expect.Fail("Error reading code.txt file: " + err.Error()) + return expect.ValidationFeedback + } + + code, ok := strings.CutPrefix(string(fileContent), codingDescription+textFileDescription+"\n") + if !ok { + expect.Fail("The content of code.txt does not match the expected format. Please ensure the file contains the correct instructions and code.") + return expect.ValidationFeedback + } + + expect.Assert(strings.Contains(code, "go.mongodb.org/mongo-driver v1.17.9"), + "The go.mod file should still contain the old version of the MongoDB Driver (v1.17.8). Please make sure to update it to the latest version (v1.17.9).") + + if !isInProgress { + return expect.ValidationFeedback + } else if issue.Status != models.StatusClosed { + expect.Fail("Issue should be set to Closed once the work is completed") + } + return expect.Complete() } From 817dbc57b7265bcadc482bb9ea48dd93b2b1988c Mon Sep 17 00:00:00 2001 From: Robin Olsen <129996395+Telikz@users.noreply.github.com> Date: Wed, 4 Mar 2026 17:32:13 +0100 Subject: [PATCH 27/51] Update cmd/pm/tasks/codingTask.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- cmd/pm/tasks/codingTask.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/pm/tasks/codingTask.go b/cmd/pm/tasks/codingTask.go index c06cb22..f73083c 100644 --- a/cmd/pm/tasks/codingTask.go +++ b/cmd/pm/tasks/codingTask.go @@ -180,7 +180,7 @@ func (t *CodingTask) Validate(ctx context.Context) ValidationFeedback { } expect.Assert(strings.Contains(code, "go.mongodb.org/mongo-driver v1.17.9"), - "The go.mod file should still contain the old version of the MongoDB Driver (v1.17.8). Please make sure to update it to the latest version (v1.17.9).") + "The go.mod snippet in code.txt should contain the updated MongoDB Driver version (v1.17.9). Please make sure you have updated the dependency in the snippet accordingly.") if !isInProgress { return expect.ValidationFeedback From 077cbc96e8bf1f9d3295a774a4331c1a90f79e43 Mon Sep 17 00:00:00 2001 From: Robin Olsen <129996395+Telikz@users.noreply.github.com> Date: Wed, 4 Mar 2026 17:32:37 +0100 Subject: [PATCH 28/51] Update cmd/pm/tasks/codingTask.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- cmd/pm/tasks/codingTask.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cmd/pm/tasks/codingTask.go b/cmd/pm/tasks/codingTask.go index f73083c..009a013 100644 --- a/cmd/pm/tasks/codingTask.go +++ b/cmd/pm/tasks/codingTask.go @@ -186,6 +186,9 @@ func (t *CodingTask) Validate(ctx context.Context) ValidationFeedback { return expect.ValidationFeedback } else if issue.Status != models.StatusClosed { expect.Fail("Issue should be set to Closed once the work is completed") + } else { + expect.Assert(t.setupIssue.Status == models.StatusClosed, + "The original setup issue should be set to Closed once the work is completed") } return expect.Complete() From f0025f8929a96cd35ff626d33d07b1080725ece2 Mon Sep 17 00:00:00 2001 From: Robin Olsen <129996395+Telikz@users.noreply.github.com> Date: Wed, 4 Mar 2026 17:32:55 +0100 Subject: [PATCH 29/51] Update cmd/pm/tasks/codingTask.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- cmd/pm/tasks/codingTask.go | 1 + 1 file changed, 1 insertion(+) diff --git a/cmd/pm/tasks/codingTask.go b/cmd/pm/tasks/codingTask.go index 009a013..4866145 100644 --- a/cmd/pm/tasks/codingTask.go +++ b/cmd/pm/tasks/codingTask.go @@ -131,6 +131,7 @@ func (t *CodingTask) Validate(ctx context.Context) ValidationFeedback { } expect.Assert(t.setupIssue.Assignee == "Me", "The original issue should be assigned to 'Me'. Please assign the issue to yourself.") + expect.Assert(t.setupIssue.Status == models.StatusInProgress, "The original issue should be marked as In Progress before starting work. Please update the issue status to In Progress.") expect.Assert(len(issues) > 0, "No new issues created. Please create an issue with the specified details.") if len(issues) == 0 { From 003fae4193e3bbb2865d4f0dfa313d2095c9a9bb Mon Sep 17 00:00:00 2001 From: Robin Olsen <129996395+Telikz@users.noreply.github.com> Date: Wed, 4 Mar 2026 17:33:08 +0100 Subject: [PATCH 30/51] Update cmd/pm/tasks/codingTask.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- cmd/pm/tasks/codingTask.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/pm/tasks/codingTask.go b/cmd/pm/tasks/codingTask.go index 4866145..fc51621 100644 --- a/cmd/pm/tasks/codingTask.go +++ b/cmd/pm/tasks/codingTask.go @@ -12,7 +12,7 @@ import ( const codingDescription = `You are tasked with doing a chore in the codebase. -This task will test your ablity to read and understand instructions, change text, and save it to a file. +This task will test your ability to read and understand instructions, change text, and save it to a file. The MongoDB Driver dependency in the file is outdated and needs to be updated to the latest version. This is a common task for developers, and it requires attention to detail and the ability to follow instructions carefully. From 1f51336288867b10cea7878ec56c33c9c94acd61 Mon Sep 17 00:00:00 2001 From: Robin Olsen <129996395+Telikz@users.noreply.github.com> Date: Wed, 4 Mar 2026 17:33:18 +0100 Subject: [PATCH 31/51] Update cmd/pm/tasks/codingTask.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- cmd/pm/tasks/codingTask.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/pm/tasks/codingTask.go b/cmd/pm/tasks/codingTask.go index fc51621..93b8710 100644 --- a/cmd/pm/tasks/codingTask.go +++ b/cmd/pm/tasks/codingTask.go @@ -18,7 +18,7 @@ The MongoDB Driver dependency in the file is outdated and needs to be updated to This is a common task for developers, and it requires attention to detail and the ability to follow instructions carefully. Your task: -1. Assing this Issue to yourself as "Me" and mark it as "In Progress" +1. Assign this Issue to yourself as "Me" and mark it as "In Progress" 2. Create a New Issue and give it these details: - Title: "Upgrade MongoDB Driver Dependency" - Description: "We need to upgrade the MongoDB Driver dependency to the latest version." From 4f70142316c69e26740005d093f8c8de7912f1bf Mon Sep 17 00:00:00 2001 From: Robin Olsen <129996395+Telikz@users.noreply.github.com> Date: Wed, 4 Mar 2026 17:33:30 +0100 Subject: [PATCH 32/51] Update cmd/pm/tasks/codingTask.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- cmd/pm/tasks/codingTask.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/pm/tasks/codingTask.go b/cmd/pm/tasks/codingTask.go index 93b8710..123f0e4 100644 --- a/cmd/pm/tasks/codingTask.go +++ b/cmd/pm/tasks/codingTask.go @@ -24,7 +24,7 @@ Your task: - Description: "We need to upgrade the MongoDB Driver dependency to the latest version." - Status: "In Progress" - Issue Type: "Chore" -3. A file will appear on your desktop named "code.txt". +3. A file will appear in the current directory named "code.txt". Open it and follow the instructions inside. And save the file after you are done. 4. When you are done, mark this and the issue you made as "Closed".` From a907de5b940f8088d0aa51b5d1e524b35a3b6ca0 Mon Sep 17 00:00:00 2001 From: Robin Olsen <129996395+Telikz@users.noreply.github.com> Date: Wed, 4 Mar 2026 17:33:48 +0100 Subject: [PATCH 33/51] Update cmd/pm/tasks/codingTask.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- cmd/pm/tasks/codingTask.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/pm/tasks/codingTask.go b/cmd/pm/tasks/codingTask.go index 123f0e4..388d6b0 100644 --- a/cmd/pm/tasks/codingTask.go +++ b/cmd/pm/tasks/codingTask.go @@ -95,7 +95,7 @@ func (t *CodingTask) Questions(interfaceType InterfaceType) (questions Questions } func (t *CodingTask) QuestionnaireKeys(interfaceType InterfaceType) []string { - keys := []string{"task_completed", "task_difficulty", "final_confirmation"} + keys := []string{"task_completed", "task_difficulty"} return keys } From 6e1c46948a0067c53b816c9c7e8a7413ae12c10e Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Wed, 4 Mar 2026 17:49:32 +0100 Subject: [PATCH 34/51] expand width of task text box --- pkg/task/taskui.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/task/taskui.go b/pkg/task/taskui.go index cfdbea0..2f7086b 100644 --- a/pkg/task/taskui.go +++ b/pkg/task/taskui.go @@ -69,7 +69,7 @@ func (m TaskModel) View() string { style.TextStyle.Render("Terminal too small.")) } - boxWidth := min(m.width-10, 80) + boxWidth := min(m.width-10, 120) detailsText := fmt.Sprintf("Time to complete: %s | Difficulty: %s", m.TimeToComplete, m.Difficulty) From 44d4b0e543fb4343649d1abfd1f2232e470296d6 Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Wed, 4 Mar 2026 17:49:49 +0100 Subject: [PATCH 35/51] make sure we get the checks --- pkg/repl/repl.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/repl/repl.go b/pkg/repl/repl.go index 091d0f0..607100d 100644 --- a/pkg/repl/repl.go +++ b/pkg/repl/repl.go @@ -129,6 +129,7 @@ func (r *REPL) watchValidation() { r.app.CurrentFeedback = &ValidationFeedback{ Success: feedback.Success, Message: feedback.Message, + Checks: feedback.Checks, } } if feedback.Success { From 5a8b9fc9fbeef1ef5d5510073a33398320d9544c Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Wed, 4 Mar 2026 17:50:00 +0100 Subject: [PATCH 36/51] print the checks --- internal/commands/survey/status.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/internal/commands/survey/status.go b/internal/commands/survey/status.go index 2168d18..0e5a954 100644 --- a/internal/commands/survey/status.go +++ b/internal/commands/survey/status.go @@ -25,6 +25,13 @@ func runStatusCmd(cmd *cobra.Command, args []string) error { return nil } - cmd.Print(app.CurrentFeedback.Message) + cmd.Println(app.CurrentFeedback.Message) + for _, check := range app.CurrentFeedback.Checks { + if check.Valid { + cmd.Printf("✅ %s\n", check.Message) + } else { + cmd.Printf("❌ %s\n", check.Message) + } + } return nil } From 7d2c00c582f2f384e2a36274fa5aded57cafc2ed Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Wed, 4 Mar 2026 17:50:24 +0100 Subject: [PATCH 37/51] update coding task for better readability --- cmd/pm/tasks/codingTask.go | 40 +++++++++++++++++++++++++------------- 1 file changed, 26 insertions(+), 14 deletions(-) diff --git a/cmd/pm/tasks/codingTask.go b/cmd/pm/tasks/codingTask.go index 388d6b0..5df0ec3 100644 --- a/cmd/pm/tasks/codingTask.go +++ b/cmd/pm/tasks/codingTask.go @@ -122,6 +122,8 @@ func (t *CodingTask) Setup(ctx context.Context) error { return nil } +var codeTaskInProgress = false + func (t *CodingTask) Validate(ctx context.Context) ValidationFeedback { expect := check.NewExpector() @@ -130,9 +132,14 @@ func (t *CodingTask) Validate(ctx context.Context) ValidationFeedback { return expect.ValidationFeedback } - expect.Assert(t.setupIssue.Assignee == "Me", "The original issue should be assigned to 'Me'. Please assign the issue to yourself.") - expect.Assert(t.setupIssue.Status == models.StatusInProgress, "The original issue should be marked as In Progress before starting work. Please update the issue status to In Progress.") - expect.Assert(len(issues) > 0, "No new issues created. Please create an issue with the specified details.") + expect.Assert(t.setupIssue.Assignee == "Me", + "The original issue should be assigned to 'Me'.") + + expect.Assert(t.setupIssue.Status == models.StatusInProgress, + "The original issue should be marked as In Progress before starting work.") + + expect.Assert(len(issues) > 0, + "No new issues created. Please create an issue with the specified details.") if len(issues) == 0 { expect.Fail("No new issues created") @@ -141,15 +148,20 @@ func (t *CodingTask) Validate(ctx context.Context) ValidationFeedback { issue := issues[0] - expect.Assert(len(issues) < 2, "Multiple issues were created instead of one. Delete the extra issues and try again.") + expect.Assert(len(issues) < 2, + "Multiple issues were created instead of one. Delete the extra issues and try again.") + + expect.NotEmptyString(issue.Title, + "Issue title should not be empty") - expect.NotEmptyString(issue.Title, "Issue title should not be empty") expect.Assert(issue.Title == "Upgrade MongoDB Driver Dependency", fmt.Sprintf("Issue title does not match the expected value 'Upgrade MongoDB Driver Dependency', but was '%s'", issue.Title)) - expect.NotEmptyString(issue.Description, "Issue description should not be empty") + expect.NotEmptyString(issue.Description, + "Issue description should not be empty") + expect.Assert(issue.Description == "We need to upgrade the MongoDB Driver dependency to the latest version.", - fmt.Sprintf("Issue description does not match the expected value 'We need to upgrade the MongoDB Driver dependency to the latest version.', but was '%s'", issue.Description)) + fmt.Sprintf("Issue description does not match 'We need to upgrade the MongoDB Driver dependency to the latest version.', but was '%s'", issue.Description)) expect.Assert(issue.IssueType == models.TypeChore, fmt.Sprintf("Issue type should be 'Chore', but was '%s'", issue.IssueType)) @@ -157,14 +169,14 @@ func (t *CodingTask) Validate(ctx context.Context) ValidationFeedback { expect.Assert(issue.Assignee == "Me", fmt.Sprintf("Issue should be assigned to 'Me', but was assigned to '%s'", issue.Assignee)) - if issue.Status == models.StatusInProgress || isInProgress { - isInProgress = true + if issue.Status == models.StatusInProgress || codeTaskInProgress { + codeTaskInProgress = true } else { - expect.Fail("Issue should be marked as in-progress when work starts") + expect.Fail("Issue should be marked as In Progress when work starts") } if _, err := os.Stat("./code.txt"); os.IsNotExist(err) { - expect.Fail("The code.txt file should exist on the desktop. Please create the file with the specified content.") + expect.Fail("The code.txt file should exist on the desktop.") return expect.ValidationFeedback } @@ -176,14 +188,14 @@ func (t *CodingTask) Validate(ctx context.Context) ValidationFeedback { code, ok := strings.CutPrefix(string(fileContent), codingDescription+textFileDescription+"\n") if !ok { - expect.Fail("The content of code.txt does not match the expected format. Please ensure the file contains the correct instructions and code.") + expect.Fail("The content of code.txt does not match the expected format.") return expect.ValidationFeedback } expect.Assert(strings.Contains(code, "go.mongodb.org/mongo-driver v1.17.9"), - "The go.mod snippet in code.txt should contain the updated MongoDB Driver version (v1.17.9). Please make sure you have updated the dependency in the snippet accordingly.") + "The MongoDB Driver dependency should be updated to version v1.17.9 in the file.") - if !isInProgress { + if !codeTaskInProgress { return expect.ValidationFeedback } else if issue.Status != models.StatusClosed { expect.Fail("Issue should be set to Closed once the work is completed") From 785c194c537dae38a61730e3f13eafa9de220041 Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Wed, 4 Mar 2026 19:20:51 +0100 Subject: [PATCH 38/51] add data analisys notebook --- notebooks/task-data-analysis.ipynb | 2421 ++++++++++++++++++++++++++++ 1 file changed, 2421 insertions(+) create mode 100644 notebooks/task-data-analysis.ipynb diff --git a/notebooks/task-data-analysis.ipynb b/notebooks/task-data-analysis.ipynb new file mode 100644 index 0000000..4765eac --- /dev/null +++ b/notebooks/task-data-analysis.ipynb @@ -0,0 +1,2421 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Task Data Analysis Notebook\n", + "\n", + "This notebook loads task run stats from `./.pm` and gives a quick view of completion rates, validation behavior, timings, and common failure patterns." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "8626d9d2", + "metadata": {}, + "outputs": [], + "source": [ + "from pathlib import Path\n", + "import json\n", + "\n", + "import pandas as pd\n", + "import matplotlib.pyplot as plt\n", + "\n", + "pd.set_option(\"display.max_columns\", 200)\n", + "plt.style.use(\"ggplot\")" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "7bc3e98d", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Working directory: /home/robino/Projects/Go/LazyPM/notebooks\n", + "Using stats directory: /home/robino/Projects/Go/LazyPM/.pm\n", + "Found 1 stats file(s).\n" + ] + }, + { + "data": { + "text/plain": [ + "[PosixPath('/home/robino/Projects/Go/LazyPM/.pm/coding-task-stats.json')]" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "candidate_dirs = [\n", + " Path.cwd(),\n", + " Path.cwd() / \"./.pm\",\n", + " Path.cwd().parent / \"./.pm\",\n", + "]\n", + "\n", + "stats_dir = None\n", + "stat_files = []\n", + "for d in candidate_dirs:\n", + " if d.is_dir():\n", + " files = sorted(d.glob(\"*-stats.json\"))\n", + " if files:\n", + " stats_dir = d\n", + " stat_files = files\n", + " break\n", + "\n", + "print(f\"Working directory: {Path.cwd()}\")\n", + "print(f\"Using stats directory: {stats_dir}\")\n", + "print(f\"Found {len(stat_files)} stats file(s).\")\n", + "stat_files" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "c453e1ad", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Loaded 1 task dataset(s).\n" + ] + } + ], + "source": [ + "datasets = []\n", + "for file in stat_files:\n", + " with file.open() as f:\n", + " data = json.load(f)\n", + " data[\"_file\"] = file.name\n", + " datasets.append(data)\n", + "\n", + "print(f\"Loaded {len(datasets)} task dataset(s).\")" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "cbb0dd6b", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "((1, 19), (19, 18), (2263, 12))" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "summary_rows = []\n", + "run_rows = []\n", + "log_rows = []\n", + "\n", + "for ds in datasets:\n", + " task_name = ds.get(\"task_name\")\n", + " file_name = ds.get(\"_file\")\n", + "\n", + " summary = ds.get(\"summary\", {}).copy()\n", + " summary.update({\"task_name\": task_name, \"file\": file_name})\n", + " summary_rows.append(summary)\n", + "\n", + " for run in ds.get(\"runs\", []):\n", + " row = run.copy()\n", + " row.update({\"task_name\": task_name, \"file\": file_name})\n", + " run_rows.append(row)\n", + "\n", + " for log in run.get(\"logs\", []):\n", + " lrow = log.copy()\n", + " lrow.update({\n", + " \"task_name\": task_name,\n", + " \"file\": file_name,\n", + " \"run_id\": run.get(\"run_id\"),\n", + " \"run_completed\": run.get(\"completed\"),\n", + " })\n", + " log_rows.append(lrow)\n", + "\n", + "summary_df = pd.DataFrame(summary_rows)\n", + "runs_df = pd.DataFrame(run_rows)\n", + "logs_df = pd.DataFrame(log_rows)\n", + "\n", + "for col in [\"updated_at\", \"first_run_started_at\", \"last_run_started_at\", \"last_run_ended_at\"]:\n", + " if col in summary_df.columns:\n", + " summary_df[col] = pd.to_datetime(summary_df[col], errors=\"coerce\")\n", + "\n", + "for col in [\"started_at\", \"ended_at\"]:\n", + " if col in runs_df.columns:\n", + " runs_df[col] = pd.to_datetime(runs_df[col], errors=\"coerce\")\n", + "\n", + "if \"timestamp\" in logs_df.columns:\n", + " logs_df[\"timestamp\"] = pd.to_datetime(logs_df[\"timestamp\"], errors=\"coerce\")\n", + "\n", + "summary_df.shape, runs_df.shape, logs_df.shape" + ] + }, + { + "cell_type": "markdown", + "id": "c974d4f8", + "metadata": {}, + "source": [ + "## Task-level summary" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "92aea6d2", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
task_nametotal_runscompleted_runsincomplete_runsaverage_duration_msvalidation_attemptsvalidation_successesvalidation_failurestotal_user_actionscompletion_rate_pctavg_duration_s
0Coding Task19118710813431342595.371.08
\n", + "
" + ], + "text/plain": [ + " task_name total_runs completed_runs incomplete_runs \\\n", + "0 Coding Task 19 1 18 \n", + "\n", + " average_duration_ms validation_attempts validation_successes \\\n", + "0 71081 343 1 \n", + "\n", + " validation_failures total_user_actions completion_rate_pct \\\n", + "0 342 59 5.3 \n", + "\n", + " avg_duration_s \n", + "0 71.08 " + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "summary_cols = [\n", + " \"task_name\",\n", + " \"total_runs\",\n", + " \"completed_runs\",\n", + " \"incomplete_runs\",\n", + " \"average_duration_ms\",\n", + " \"validation_attempts\",\n", + " \"validation_successes\",\n", + " \"validation_failures\",\n", + " \"total_user_actions\",\n", + "]\n", + "\n", + "summary_view = summary_df.reindex(columns=summary_cols).copy()\n", + "\n", + "if not summary_view.empty:\n", + " total_runs_nonzero = summary_view[\"total_runs\"].replace({0: pd.NA})\n", + " summary_view[\"completion_rate_pct\"] = (summary_view[\"completed_runs\"] / total_runs_nonzero * 100).round(1)\n", + " summary_view[\"avg_duration_s\"] = (summary_view[\"average_duration_ms\"] / 1000).round(2)\n", + "\n", + "summary_view.sort_values(\"completion_rate_pct\", ascending=False)" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "6bd0a081", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAxYAAAGGCAYAAADmRxfNAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjgsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvwVt1zgAAAAlwSFlzAAAPYQAAD2EBqD+naQAANpNJREFUeJzt3X18z3X////7e3a+YdjYZrNh5GxMVAclhiEHHZ3I+UFJpajUgUJHpk+EUihyJDk7QuNAitAJk/McQ87K6Q4NY7ONjLGT1+8P371/3m1jPKe3k9v1culS7+fr9Xq+Hq/XU/W+v1/P1+tlsyzLEgAAAAAYcHF2AQAAAABufQQLAAAAAMYIFgAAAACMESwAAAAAGCNYAAAAADBGsAAAAABgjGABAAAAwBjBAgAAAIAxggUAAAAAYwQLALhDxMbGymazac2aNTd0Py1atJDNZruh+7iT/FnjdrMJDw9XeHi4s8sAcA0IFgDw//zyyy968cUXVa9ePZUtW1bu7u4KDg7WX//6V02fPl0XLlxwdok3hSeffFI2m02JiYnOLsVpEhMTZbPZ9OSTTzq7lBvqTg01AK6Pq7MLAICbwVtvvaWRI0cqLy9PTZo0Ue/eveXr66sTJ05ozZo16tu3rz7++GNt3brV2aXe9GbPnq1z5845uwwAwJ+MYAHgjjd69GiNGDFCoaGhWrBgge67774C63z99dcaP368E6q79VSpUsXZJQAAnICpUADuaImJiYqNjZWbm5uWL19eaKiQpA4dOmjFihUF2uPi4vTggw+qbNmy8vLyUmRkpN55551Cp03lzxk/e/asXnnlFYWGhsrLy0tRUVFasmSJJCknJ0ejRo1SjRo15OnpqerVq+ujjz4q0NeaNWtks9kUGxurjRs3qnXr1ipbtqxKly6ttm3bXvOVlV9++UVPPvmkQkND5e7urkqVKql79+769ddfHdaz2WyaNWuWJKlq1aqy2Wyy2WwOc+GLusciLy9PU6dO1T333CNfX1/5+Pjonnvu0ccff6y8vLwC69tsNrVo0UKpqal69tlnFRQUJA8PD9WtW1czZsy4puPLP/dnzpzRq6++qvDwcLm5uSk2NlaSdOzYMb311lu6//77FRgYaJ8G1717d+3Zs8ehr9jYWFWtWlWSNGvWLPs5sNlsmjlzpsO6K1euVPv27eXv7y8PDw9Vr15dgwcPVkZGxjXVn2/WrFlq2LChvLy8VLFiRfXp00fJyckO6zRp0kQuLi5FTlUbP368bDab3nvvvSvuKzw8XCNHjpQkRUdHOxxnvn379un1119X48aNFRAQIA8PD4WFhenZZ59VUlJSgT4ty9KsWbPUtGlTBQQEyNPTU6GhoWrbtq2++OKLYp2DuXPnysPDQ7Vr176jp+MBNyOuWAC4o82YMUPZ2dnq2rWr6tWrd8V1PTw8HD4PGzZM77zzjvz9/dW9e3f5+vrqm2++0bBhw7Ry5UqtWrVK7u7uDttkZ2crJiZGaWlp+tvf/qaLFy9q3rx5evzxx7Vq1SpNmTJFmzdv1kMPPSQPDw8tWLBAL774ogICAtSlS5cCNW3evFnvvPOOWrdurf79++vAgQNatGiR1q5dq1WrVqlZs2ZXPQcrVqzQY489puzsbHXs2FERERFKSkrSokWLtGzZMq1evVp33323JGnEiBFasmSJduzYoZdffll+fn6SZP/7lfz973/X3LlzFRoaqr59+8pms2nx4sV64YUXtG7dOn3++ecFtsnIyND9998vd3d3derUSRcuXNCCBQvUp08fubi4qHfv3lfdb76LFy+qZcuWSktLU5s2bVSmTBl7QFi7dq3GjBmj6OhoPf744/L19dX+/fu1cOFCLV26VOvXr1eDBg0kXQpOGRkZmjhxoho0aKBHHnnEvo+oqCj7P48cOVKxsbEqX768OnTooIoVK+rnn3/We++9p+XLl2vjxo0qU6ZMsev/4IMPtGrVKnXp0kXt2rXTunXrNGPGDK1Zs0abN29WQECAJOn555/Xpk2bNG3aNI0aNapAP5988ok8PDyuen/IwIEDtWTJEsXHx6t3796F3ki9aNEiTZ06VdHR0WratKnc3d21e/duffrpp/rqq6+0detWVa5c2b7+8OHD9c4776hq1arq3LmzypYtq+PHj+unn37SggULCv0zfrlx48bp9ddfV9OmTbV06VKVL1/+6icOwJ/HAoA7WMuWLS1J1rRp065puw0bNliSrNDQUOv48eP29uzsbKtDhw6WJGvUqFEO24SFhVmSrA4dOlhZWVn29rVr11qSrHLlylmNGze20tPT7csOHjxoubm5WVFRUQ59rV692pJkSbI+/PBDh2VLliyxJFkRERFWbm6uvX3EiBGWJGv16tX2trS0NMvPz8+qUKGC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Xbx4UX/961+LfP/DihUr9NBDD9k/9+nTR5L09ttvKyUlxd6em5urQYMGKS8v75qugFyv/fv3a8qUKQ5tX375peLj4xUREXHVx80+9dRT8vPz08iRI7Vly5YCy/Py8gqEp/ypO0eOHCl2nfnna+jQoQ5v5T537pxef/11SfpTzldhKlasKG9vb/33v/91mPKVnZ2tl19+WampqQW2KVeunGw2W5Hn4JVXXpEkPfPMMzp27FiB5ZmZmdq0adM11TlnzpwC96HExsbq9OnT6tatW4HHIbu4uOjZZ5/VyZMn7ee/X79+17TPK411/uNn161b5xA8zp49q2eeeabAPSoXLlzQ+vXrC/STnZ1tv7Lj7e1daB3NmjXTt99+K5vNpjZt2hQasgA4H1OhANzxhg0bZp8Pf88996hp06Zq3LixfH19deLECa1du1b79+93mILRtGlTDRkyROPGjVO9evXUqVMn+fj46JtvvtGuXbv0wAMPaPDgwTe89nbt2ukf//iHvvnmGzVo0MD+HgtPT0999tlnhd7YfbkKFSpo4cKFevTRR/WXv/xFrVq1Ut26dWWz2fTbb79p48aNOnXqlLKysuzbtGrVSu+++66eeeYZPf744ypdurT8/Pw0YMCAIvfTvXt3ffnll4qLi1PdunX1yCOP2O9fOHz4sLp06aIePXqU2Hm5Fi4uLnrppZc0ZswYRUZG2t8vsnr1aqWlpSk6OlqrV6922MbX11f33XeffvzxR/Xo0UM1a9ZUqVKl9PDDD6t+/fpq1aqVxowZo6FDh6pGjRpq3769qlatqrNnz+p///uf4uPj9cADDxT60sWiPPTQQ7r//vvVuXNnBQUFad26dVq3bp3Cw8M1ZsyYQrfp27ev3nrrLR09elSRkZFq0qTJNZ2b6Ohoubi4aOjQodq1a5f9ascbb7yhwMBAde3aVfPnz1dUVJTatGmj06dP69tvv5Wnp6eioqK0fft2e1/nz5/XAw88oIiICDVq1EhhYWHKysrSt99+q7179+rhhx8ucNXlcvfdd59++OEHxcTEqH379lqyZIliYmKu6XgA3GDOfiwVANws9uzZYw0YMMCqW7euVbp0acvNzc0KDAy02rVrZ3366aeFPiJz3rx51v3332/5+vpaHh4eVp06day333670OfsX/5c/j8q7BGt+Qp7vGv+42ZHjBhhbdiwwWrV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jRqpd+/e6tixo44cOaLY2FilpaUV67h27dqlESNG6Pz583riiSfUrVs3nTt3Tm+99ZYOHDhwxZrS0tKUmprqEAAlKSIiQomJiVq3bp1OnjypRYsWKTAwUL6+vjpz5owWLlyo3r17X/UqULVq1XTkyBGdO3euWOcIwJ2LKxYAcJM5d+6c0tLS1Lhx42Ktn5iYqPj4eLVs2VL9+vWTJLVt21Zly5bVV199pV27dqlevXr29Y8dO6a3335bNWvWlHTpJt1Ro0bpX//6lyZMmCB/f39Jl37h/uSTT7R3717VrVvXvn12drYaNGigPn362Pc1duxYffnll3rooYeKnFYzf/585eXl6b333lPp0qUlSW3atNGECRO0YMECxcTEyM/PTw0bNlRcXJxq1qypBx988IrHvnXrVu3atUtdu3bVY489JunSVLH3339f33zzjdq1a6fAwED7+kePHtX7779vb6tbt64GDx6s9evXX/GX+8uPfcyYMQ43zVepUkUTJ050mBL14IMP6pVXXtEPP/ygTp06XfG4LMvStGnTVLduXQ0bNsx+hSEmJkavvvqq5s+frzfeeKPImo4ePSpJqlixokN7rVq11K5dO02aNEnSpfF89dVXJV260btGjRq6++67r3rMFStWlGVZOnbsmCIiIq66PoA7F1csAOAmc/78eUmSl5dXsdbftm2bJKlDhw4O7R07dpSkAvdihISE2EOFJPu8/Hr16tlDhST7l8gTJ04U2OflX8JtNpvatWunnJwc7dy5s9AaLcvS5s2b1ahRI1mWpTNnztj/ioqK0rlz53To0KFiHe/ltm3bJhcXFz300EMO7R06dJBlWdq+fbtDe2RkpEPQCAsLk5eXV6HHWJjmzZsXeBKXm5ubPVTk5eXp999/l6enp4KDg3X48OGr9pmYmKjjx4/rgQce0O+//24/L1lZWapXr5727t1b4KrR5fKvtvj4+BRY9tRTT2nKlCkaPXq0pkyZonr16ikxMVFr165V7969de7cOU2aNEnPPfecYmNjlZSUVKAPX19fSdKZM2eueiwA7mxcsQCAm0x+oMgPGFeTkpIim83m8IVZkvz8/OTj46PU1FSH9svDgyR5e3tLkipUqFBoe2ZmpkO7zWZTpUqVHNqCgoLstRTmzJkzyszM1HfffafvvvuuyHWuVUpKisqVK1cghIWEhBRazx+P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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "if not summary_view.empty and summary_view[\"completion_rate_pct\"].notna().any():\n", + " plot_df = summary_view[[\"task_name\", \"completion_rate_pct\"]].sort_values(\"completion_rate_pct\")\n", + " ax = plot_df.plot(kind=\"barh\", x=\"task_name\", y=\"completion_rate_pct\", legend=False, figsize=(8, 4))\n", + " ax.set_xlabel(\"Completion rate (%)\")\n", + " ax.set_ylabel(\"\")\n", + " ax.set_title(\"Completion rate by task\")\n", + " plt.tight_layout()\n", + "else:\n", + " print(\"No summary data available for plotting.\")" + ] + }, + { + "cell_type": "markdown", + "id": "8d7da541", + "metadata": {}, + "source": [ + "## Run-level diagnostics" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "694e0476", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
task_namerun_idinterface_typecompletedduration_msvalidation_attemptsvalidation_successesvalidation_failuresquestionnaire_completedduration_s
0Coding Task1webTrue9014384183True90.14
1Coding Task2replFalse28458000False28.46
2Coding Task3replFalse10512000False10.51
3Coding Task4replFalse8732978078True87.33
4Coding Task5replFalse4171440040True41.71
5Coding Task6replFalse5493953053True54.94
6Coding Task7replFalse3962638038True39.63
7Coding Task8replFalse4816046046True48.16
8Coding Task9replFalse11927202True11.93
9Coding Task10replFalse258557202True258.56
10Coding Task11replFalse40334000False40.33
11Coding Task12replFalse21621000True21.62
12Coding Task13replFalse72097000True72.10
13Coding Task14replFalse9530000True9.53
14Coding Task15webFalse523000False0.52
15Coding Task16replFalse101759000True101.76
16Coding Task17replFalse114763000True114.76
17Coding Task18replFalse55497000True55.50
18Coding Task19replFalse263061000True263.06
\n", + "
" + ], + "text/plain": [ + " task_name run_id interface_type completed duration_ms \\\n", + "0 Coding Task 1 web True 90143 \n", + "1 Coding Task 2 repl False 28458 \n", + "2 Coding Task 3 repl False 10512 \n", + "3 Coding Task 4 repl False 87329 \n", + "4 Coding Task 5 repl False 41714 \n", + "5 Coding Task 6 repl False 54939 \n", + "6 Coding Task 7 repl False 39626 \n", + "7 Coding Task 8 repl False 48160 \n", + "8 Coding Task 9 repl False 11927 \n", + "9 Coding Task 10 repl False 258557 \n", + "10 Coding Task 11 repl False 40334 \n", + "11 Coding Task 12 repl False 21621 \n", + "12 Coding Task 13 repl False 72097 \n", + "13 Coding Task 14 repl False 9530 \n", + "14 Coding Task 15 web False 523 \n", + "15 Coding Task 16 repl False 101759 \n", + "16 Coding Task 17 repl False 114763 \n", + "17 Coding Task 18 repl False 55497 \n", + "18 Coding Task 19 repl False 263061 \n", + "\n", + " validation_attempts validation_successes validation_failures \\\n", + "0 84 1 83 \n", + "1 0 0 0 \n", + "2 0 0 0 \n", + "3 78 0 78 \n", + "4 40 0 40 \n", + "5 53 0 53 \n", + "6 38 0 38 \n", + "7 46 0 46 \n", + "8 2 0 2 \n", + "9 2 0 2 \n", + "10 0 0 0 \n", + "11 0 0 0 \n", + "12 0 0 0 \n", + "13 0 0 0 \n", + "14 0 0 0 \n", + "15 0 0 0 \n", + "16 0 0 0 \n", + "17 0 0 0 \n", + "18 0 0 0 \n", + "\n", + " questionnaire_completed duration_s \n", + "0 True 90.14 \n", + "1 False 28.46 \n", + "2 False 10.51 \n", + "3 True 87.33 \n", + "4 True 41.71 \n", + "5 True 54.94 \n", + "6 True 39.63 \n", + "7 True 48.16 \n", + "8 True 11.93 \n", + "9 True 258.56 \n", + "10 False 40.33 \n", + "11 True 21.62 \n", + "12 True 72.10 \n", + "13 True 9.53 \n", + "14 False 0.52 \n", + "15 True 101.76 \n", + "16 True 114.76 \n", + "17 True 55.50 \n", + "18 True 263.06 " + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "run_cols = [\n", + " \"task_name\",\n", + " \"run_id\",\n", + " \"interface_type\",\n", + " \"completed\",\n", + " \"duration_ms\",\n", + " \"validation_attempts\",\n", + " \"validation_successes\",\n", + " \"validation_failures\",\n", + " \"questionnaire_completed\",\n", + "]\n", + "\n", + "runs_view = runs_df.reindex(columns=run_cols).copy()\n", + "if \"duration_ms\" in runs_view.columns:\n", + " runs_view[\"duration_s\"] = (runs_view[\"duration_ms\"] / 1000).round(2)\n", + "\n", + "runs_view.sort_values([\"task_name\", \"run_id\"]).head(20)" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "7e0aae07", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
runscompleted_runsavg_duration_smedian_duration_savg_validation_attemptscompletion_rate_pct
task_name
Coding Task19171.0848.1618.055.3
\n", + "
" + ], + "text/plain": [ + " runs completed_runs avg_duration_s median_duration_s \\\n", + "task_name \n", + "Coding Task 19 1 71.08 48.16 \n", + "\n", + " avg_validation_attempts completion_rate_pct \n", + "task_name \n", + "Coding Task 18.05 5.3 " + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "if not runs_df.empty and {\"task_name\", \"run_id\", \"completed\", \"duration_ms\", \"validation_attempts\"}.issubset(runs_df.columns):\n", + " agg = runs_df.groupby(\"task_name\", dropna=False).agg(\n", + " runs=(\"run_id\", \"count\"),\n", + " completed_runs=(\"completed\", \"sum\"),\n", + " avg_duration_s=(\"duration_ms\", lambda s: round(s.mean() / 1000, 2)),\n", + " median_duration_s=(\"duration_ms\", lambda s: round(s.median() / 1000, 2)),\n", + " avg_validation_attempts=(\"validation_attempts\", \"mean\"),\n", + " )\n", + "\n", + " agg[\"completion_rate_pct\"] = (agg[\"completed_runs\"] / agg[\"runs\"] * 100).round(1)\n", + " agg[\"avg_validation_attempts\"] = agg[\"avg_validation_attempts\"].round(2)\n", + " display(agg.sort_values(\"completion_rate_pct\", ascending=False))\n", + "else:\n", + " print(\"Not enough run data to build aggregate diagnostics.\")" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "25d3f1a6", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAA90AAAHaCAYAAAAdVCoXAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjgsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvwVt1zgAAAAlwSFlzAAAPYQAAD2EBqD+naQAAXnZJREFUeJzt3X1cVGX+//H3DDPcKoyIBAqCiIimgpo3X3W9S8vKvKtV07Y2k7Y0c9vdWrftztLMLf3Wam1tauXWluZmZnm3llrepKaV95IiCgoC6qCICMOc3x9+mZ8TqEhMI/B6Ph4+ZM65zjmfcwYd3lznuo7JMAxDAAAAAACg2pm9XQAAAAAAALUVoRsAAAAAAA8hdAMAAAAA4CGEbgAAAAAAPITQDQAAAACAhxC6AQAAAADwEEI3AAAAAAAeQugGAAAAAMBDCN0AAAAAAHgIoRsAcM0zmUzq3bu3t8u4aunp6TKZTPrtb3/r7VIqraJr/eyzz8pkMmnt2rVeqemdd96RyWTSO++847Y8NjZWsbGxXqmpjLevTUV++9vfymQyKT093dul/KJq6v8TAGo/QjcA1EImk8ntj4+Pj0JDQ9W7d2+98847MgzD2yXWGvygf2U1OQReKvCjYjX5vQYAT7F4uwAAgOc888wzkqSSkhIdOHBAixcv1rp16/Ttt99q9uzZXq6u9mvSpIn27t2rkJAQb5fyszz88MMaOXKkmjZt6pXjDx06VF27dlVkZKRXjn853r42AIBrH6EbAGqxZ5991u31hg0b1LNnT73++uv64x//qGbNmnmnsDrCarUqMTHR22X8bGFhYQoLC/Pa8UNCQq7ZX1x4+9oAAK593F4OAHVI9+7dlZiYKMMwtG3bNtfytWvXymQylQvpZSoaO3vxbbdr1qxR7969Vb9+fQUHB+u2227T3r17r6q24uJiPf/882revLn8/PzUrFkzPfnkkzp//nyF7S93G+ulzqd3794ymUwqLi7Wc889p5YtW8rPz8815jo/P18vvfSS+vbtq6ioKPn6+qpRo0YaNGiQNm3aVOH5S9K6devcbucvO+7lxnRnZWVp/Pjxio2NdR1n2LBhbu/LT4/lrWt9qXHLX3/9tW6//XZFRUXJz89PERER6tq1qyZPnuxqYzKZ9O6770qSmjVr5rpGF38/Xel9udIt3vn5+Xr44YfVpEkT+fv7q3Xr1vr73/9ebhjF1X6f9+7dW/fdd58k6b777nN7j8u+7y43pvuLL77QgAEDFBoaKj8/PyUkJGjSpEnKz88v17bsGjgcDr3wwgtq0aKF/Pz8FB0drT//+c8qLi6usObLcTqdmjlzphITE+Xv76+oqCg9+uijOn36tKtNaWmpoqOjFRwcrIKCggr3M2HCBJlMJi1atOiyx6vMe71t2zZNnDhRSUlJCg0Nlb+/v1q0aKE//vGPOnXqVLl9FhcX6+9//7s6dOigBg0aKDAwULGxsRo8eLBWr15dqevw0ksvyWw2q3v37jp58mSltgGA6kRPNwDUUVartVr289lnn2nJkiW65ZZb9OCDD2rPnj1atmyZtm7dqj179lSqF9AwDA0fPlxLlixR8+bN9fDDD6u4uFjz5s3Tzp07q6XOi91xxx3aunWrbrnlFg0ZMkTh4eGSpL179+qvf/2revbsqdtuu00NGjTQkSNH9Omnn2r58uVaunSpBgwYIElKTk7WM888o8mTJysmJsYtWF9pjPehQ4fUo0cPHTt2TH379tVdd92ljIwMffTRR/r888/1n//8RwMHDiy33bV0rVesWKHbbrtNwcHBGjRokJo0aaKTJ09q7969ev31111DG5555hl98skn+uGHHzRx4kTZbDZJcv19sUu9L5dTXFysfv36yW63a+TIkSouLtZ//vMfTZw4Ufv379drr71W6XP6qd/+9rey2WxasmSJBg8erOTkZNe6iuq/2JtvvqmHHnpIQUFB+vWvf63w8HCtXbtW06dP19KlS7Vhw4YK9zFq1Ch9/fXXuuWWWxQcHKxly5bpb3/7m3JycvT2229fVf2PPvqovvrqKw0fPlyDBw/WypUr9corr+jrr7/W+vXr5e/vLx8fH6WkpOiZZ57RBx98oJSUFLd9nDt3Tu+9954iIiI0ePDgyx6vMu/1W2+9pcWLF6tXr17q16+fnE6ntm3bppkzZ2r58uXavHmz6tev72r/29/+Vh988IHatGmje+65RwEBATp27JjWr1+vFStWqF+/fpesx+l06ve//71mzZqlYcOG6f3335e/v/9VXUMAqBYGAKDWkWRU9F/8unXrDLPZbPj6+hrHjh1zLV+zZo0hyXjmmWcq3F9MTIwRExPjtuztt982JBk+Pj7G6tWr3dZNmjTJkGRMnz69UvW+//77hiSja9euxrlz51zLT5w4YcTFxRmSjF69erltc++99xqSjEOHDpXb36XOp1evXoYko23btkZubm657ex2e4XLMzIyjMjISCMxMbHcuopqK3Po0CFDknHvvfe6Lb/pppsMScaUKVPclm/YsMHw8fExQkNDjTNnzriWe/taP/PMM4YkY82aNa5lw4YNMyQZ33//fblj/PQaXu69Mowrvy9l5//222+7LY+JiTEkGd27dzeKiooqPJd169a5lv+c7/OfHrtMRdcmPT3d8PX1NerXr2/s3bvXrf1DDz1kSDJSUlIqvAYdOnQwTpw44VpeUFBgNG/e3DCbzUZWVlaFNfxU2fVu2LChkZ6e7lpeWlrqet+ee+451/Jjx44ZFovF6NixY7l9lZ3/E088cVXHvtR7nZ6ebjgcjnLL58yZY0gyXnzxRdcyu91umEwmo2PHjhVuk5eX5/b64u/dc+fOuc714YcfNkpLSytVPwB4AreXA0At9uyzz+rZZ5/VX//6V40YMUL9+vWTYRh6+eWXq21SqpEjR+rGG290W/bAAw9IkrZs2VKpfZT14L3wwgtuPVGhoaF66qmnqqXOiz3//PMV9gqHhIRUuDwqKkp33nmn9u3bpyNHjvysY2dmZmrVqlVq2rSpHn/8cbd13bp101133aWTJ0/q448/LrfttXitAwICyi2r6hjnS70vVzJt2jT5+fm5Xl98LlfbO1wd3nvvPRUXF+vhhx8uN6Z/6tSpql+/vv71r39VeDv/9OnTFRoa6nodFBSk0aNHy+l06ttvv72qOiZOnKiYmBjXa7PZ7LrVet68ea7lkZGRGjJkiLZt21ZueMObb74ps9lcrge8qmJiYuTj41Nu+ZgxYxQcHKyVK1e6lplMJhmGIT8/P5nN5X9kbdiwYYXHOHnypPr166fFixdr+vTpmjVrVoXbA8Avhf+BAKAWmzx5siZPnqwXXnhBCxculMPh0Ny5czVhwoRqO8YNN9xQbll0dLQkVThGsyLbt2+X2WxWjx49yq3zxOO4OnfufMl1GzZs0PDhwxUdHS0/Pz/XuNRZs2ZJko4ePfqzjv3dd99Jkn71q19VeIt/37593dpd7Fq61qNHj5YkdenSRQ8++KAWLFigzMzMSm9fkcu9L5disVjUrVu3csvLzqWi6+hp27dvl/T/38uLNWjQQO3bt1dRUZH27dtXbn11vMdlevXqVW5ZXFycoqOjlZ6eLrvd7lo+btw4SRdCdpmdO3fqm2++0c0331xtz0MvKSnR7Nmz1aNHD4WGhsrHx0cmk0lms1mnT592+/cVHBys22+/XRs3blRycrKee+45rVmzRoWFhZfc//Hjx9W9e3dt3bpV7733XrlfbAGANzCmGwBqMeP/JpI6e/asNm3apPvvv18PPvigYmJiKgwEVVHRuFSL5cLHS2lpaaX2kZ+fr9DQ0ApDaERExM+qryKX2ufixYt15513yt/fX/3791fz5s0VFBQks9mstWvXat26dZecbKyyyibRutSdBmXLLw5EZa6laz1s2DB99tlnmjFjhubNm+cKax07dtS0adPUv3//Su+rKscvExYWVmHPadm+Kpq0zNO8/R6Xue666ypcHhERocOHDys/P991vD59+qhVq1b64IMPNGPGDNWvX1///Oc/JUm/+93vruq4lzNixAgtXrxYcXFxGjx4sCIiIlx3Kbzyyivl/n0tWLBA06dP17///W/XPAH+/v6688479fLLL5c7x+zsbJ0+fVpRUVEV/mIJALyBnm4AqAOCgoLUr18/LV26VKWlpbr33nvdeovKbr10OBwVbl9ROKhOISEhOnnypEpKSsqty87OrnCby9V8pXrLZh3/qaeeekq+vr769ttv9cknn2jGjBl67rnn9Oyzz6ply5ZXOIvKKXv01aXOKysry61ddavKtb6U2267TV9++aVOnTqlL774Qo8++qh2796tgQMHas+ePVdd26Xel8vJy8urMIyWncvF1/GX+j739ntc5vjx4xUur+jaSNKDDz6ogoICvf/++64J1Jo0aVLhpH5V8e2332rx4sXq16+f9u/fr7ffflvTpk3Ts88+q6effrrCGdoDAgL07LPPKjU1VUeOHNF7772nHj166L333tOdd95Zrn1SUpLeffddHT16VD179lRaWlq11A4APwehGwDqkHbt2iklJUWZmZn63//9X9fyBg0aSJIyMjLKbXPgwAGP9xZ26NBBTqdT69evL7euokcxSZev+WrHvpY5cOCAWrdurVatWrktv1Rt0oUgdzU9kO3bt5ckrV+/vsLwt2bNGkkXroknVOVaX0lQUJD69u2rmTNn6oknnlBxcbGWL1/uWl/WE321PbWV4XA4tHHjxnLLy86l7HpLVfs+r0rtZces6Hra7XZ9//338vf3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X39db775piZOnChJKiws1JQpU9S2bVulpKToyJEj+sc//qGgoCD169dPkrR//369+uqrGjVqlDp06KD169frpZde0vTp09W0aVNJ0pIlS7R8+XKNHz9e4eHhWrBggaZOnaqZM2fK19fXE5cHAAAAAFDDVCp0p6amKiEhoUoHuJpt27dvr/bt21+2jcVikc1mq3BdZmamvv/+e02bNk3NmzeXdGHSt2nTpuk3v/mNQkNDtX79ejkcDo0bN04Wi0XR0dFKT0/XZ5995grdy5YtU3JysgYNGiRJGjlypHbu3KkVK1bogQcekGEYWrZsmYYNG6ZOnTpJkh5++GGlpKRo69at6t69e4X1lZSUuPVom0wmBQQEuL4GAKA2Kfts4zMOAFCXVSp0T548WQkJCerfv786duwoPz+/y7YvKirSt99+q//+9786ePCg3nvvvWopVrpwC/rYsWMVFBSkNm3aaOTIkapfv76kCwE/KCjIFbglqW3btjKZTDpw4IA6d+6s1NRUtWrVyq2HPCkpSUuWLFFBQYHq1aun1NRUDRw40O24SUlJ2rp1qyQpJydHdrtd7dq1c60PDAxUfHy8UlNTLxm6Fy9erEWLFrleN2vWTNOnT6/ULQkAANRUERER3i4BAACvqVTofvXVV7Vo0SLNnj1bPj4+atGihZo1a6bw8HAFBQVJujCWOicnR2lpaTpw4IBKS0vVq1cvTZgwodqKTU5OVpcuXRQeHq7s7Gx98MEHeuGFFzR16lSZzWbZ7XYFBwe7bePj46N69erJbrdLkux2u8LDw93alPWc2+12V9uQkBC3NiEhIW77KFt2qTYVGTp0qFuYL/vNf25urhwOR2UuAQAANYbJZFJERISys7MZ0w0AqHUsFkv1jekOCwvTgw8+qFGjRumrr77St99+q1WrVqm4uNitna+vr+Li4jRy5Ej17NmzXAD+uS7uQW7atKliYmI0YcIE7d69W23btq3WY3mC1WqV1WqtcB0/jAAAaivDMPicAwDUWVc1kVpwcLAGDhyogQMHqrS0VHl5eTpz5owkqX79+goLC5OPj49HCq3Iddddp/r16ys7O1tt27aVzWbT6dOn3dqUlpaqoKDA1Ztts9nK9UaXvb64TX5+vlub/Px8t/Vlyxo0aODWJjY2tjpODQAAAABQC5iruqGPj4+uu+46xcfHKz4+Xtddd90v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AACAhxC6AQAAAADwEEI3AACoVU6dOqWvvvpKJ0+elCQ5nU4vVwQAqMsI3QAAoFbZunWrFixYoNTUVLflx44dU3p6uneKAgDUWYRuAABQK5T1aHfu3FmhoaFKS0uTJJnNZtntdj3zzDM6fPiwN0sEANRBhG4AAFBjGYah0tJSSRfCtSTZbDZFR0fr8OHDyszMlCR99NFHat68uXr16uW1WgEAdROhGwAA1Fgmk0k+Pj6SpAMHDujQoUOSpOTkZJ08eVJpaWnKycnR9u3bNXr0aG+WCgCooyzeLgAAAOBihmHIMAxXz/XlnD59WosXL9b69evl5+enLl26qFmzZkpKStKyZcuUlpamQ4cOqUuXLmrSpImkC7ehV2bfAABUB0I3AADwuouDtslkkslkksPhUGFhoYKDgy+53dKlS5Wenq6UlBS1atVKJ06ckMPhkJ+fn1q2bKmNGzcqJydHbdu21YEDB5SQkCCz2UzwBgD8Yvi0AQAAXuN0OmUYhkwmkysEb9myRU899ZQmTJigY8eOudru3r1beXl5MgxDklRUVKR169ape/fu6ty5s/z8/BQbGyuL5UKfQocOHRQYGKguXbooIiJCL7zwgmbOnKk9e/YQuAEAvxiTUfbJBQAA4CUHDhzQl19+qc2bN8tisahbt27q16+frFarFi1apC1btigoKEi+vr761a9+pWHDhqmoqEizZ89Wdna24uLi5O/vL4fDIYvForvvvlu+vr6aNm2a6tevr9/97nfKycnRggULlJaWpvr16+v+++9XfHy8t08dAFDLEboBAIDXzJ8/X19//bWKiorUtGlTpaWl6ZlnnlFiYqKcTqc++eQTpaWlaciQIYqJidHmzZu1aNEi3XnnnerRo4dOnjypBQsWyGKxqH79+jpz5ow2b96szp0764EHHtDnn3+ujRs3atiwYerYsaMkKSMjQ2fOnFHr1q29fPYAgLqAMd0AAOAXVzam2mw266677lKfPn107tw5zZgxQ6tWrVJiYqKKi4vVtm1b9e3bVzabTdnZ2Tp69KiOHz+u77//Xu3atVNoaKgeeughSVJxcbF8fX1lMpm0f/9+SVKbNm20du1anTt3znXs6Ohor5wzAKBuoqcbAAD84srGcV+spKREy5cv16effqq33nrLtf7UqVOaP3++du7cqYSEBPn7+ysjI0O/+c1v1K5dOxUW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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "if not runs_df.empty and {\"duration_ms\", \"task_name\"}.issubset(runs_df.columns):\n", + " runs_df.boxplot(column=\"duration_ms\", by=\"task_name\", figsize=(10, 5), rot=20)\n", + " plt.title(\"Run duration distribution by task\")\n", + " plt.suptitle(\"\")\n", + " plt.ylabel(\"Duration (ms)\")\n", + " plt.tight_layout()\n", + "else:\n", + " print(\"No run duration data available for boxplot.\")" + ] + }, + { + "cell_type": "markdown", + "id": "6b89c284", + "metadata": {}, + "source": [ + "## Validation check failure hotspots" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "7e75fb47", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
task_nametargetfailed_count
8Coding TaskThe original issue should be assigned to 'Me'....274
4Coding TaskNo new issues created273
5Coding TaskNo new issues created. Please create an issue ...273
10Coding TaskThe original issue should be marked as In Prog...255
6Coding TaskThe go.mod file should still contain the old v...57
0Coding TaskIssue should be assigned to 'Me', but was assi...39
2Coding TaskIssue should be set to Closed once the work is...39
1Coding TaskIssue should be marked as in-progress when wor...30
3Coding TaskIssue title does not match the expected value ...30
7Coding TaskThe original issue should be assigned to 'Me'.4
\n", + "
" + ], + "text/plain": [ + " task_name target \\\n", + "8 Coding Task The original issue should be assigned to 'Me'.... \n", + "4 Coding Task No new issues created \n", + "5 Coding Task No new issues created. Please create an issue ... \n", + "10 Coding Task The original issue should be marked as In Prog... \n", + "6 Coding Task The go.mod file should still contain the old v... \n", + "0 Coding Task Issue should be assigned to 'Me', but was assi... \n", + "2 Coding Task Issue should be set to Closed once the work is... \n", + "1 Coding Task Issue should be marked as in-progress when wor... \n", + "3 Coding Task Issue title does not match the expected value ... \n", + "7 Coding Task The original issue should be assigned to 'Me'. \n", + "\n", + " failed_count \n", + "8 274 \n", + "4 273 \n", + "5 273 \n", + "10 255 \n", + "6 57 \n", + "0 39 \n", + "2 39 \n", + "1 30 \n", + "3 30 \n", + "7 4 " + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "if not logs_df.empty and {\"action\", \"result\", \"task_name\", \"target\"}.issubset(logs_df.columns):\n", + " validation_checks = logs_df[(logs_df[\"action\"] == \"validate_check\") & (logs_df[\"result\"] == \"failed\")].copy()\n", + "\n", + " if not validation_checks.empty:\n", + " failed_by_target = (\n", + " validation_checks\n", + " .groupby([\"task_name\", \"target\"], dropna=False)\n", + " .size()\n", + " .reset_index(name=\"failed_count\")\n", + " .sort_values([\"task_name\", \"failed_count\"], ascending=[True, False])\n", + " )\n", + " display(failed_by_target.groupby(\"task_name\", dropna=False).head(10))\n", + " else:\n", + " print(\"No failed validation checks found.\")\n", + "else:\n", + " print(\"No validation logs available for failure hotspot analysis.\")" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "e44f3e85", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
task_namerun_idattempt_count
0Coding Task184
1Coding Task478
2Coding Task540
3Coding Task653
4Coding Task738
5Coding Task846
6Coding Task92
7Coding Task102
\n", + "
" + ], + "text/plain": [ + " task_name run_id attempt_count\n", + "0 Coding Task 1 84\n", + "1 Coding Task 4 78\n", + "2 Coding Task 5 40\n", + "3 Coding Task 6 53\n", + "4 Coding Task 7 38\n", + "5 Coding Task 8 46\n", + "6 Coding Task 9 2\n", + "7 Coding Task 10 2" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/png": "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W1RUVJbjMjIyZN++fSUAuX//foN9/fr1kwDk5cuX9dsuX74sAch+/foZHNuyZUsJQH777bcG21esWCEBZJvz4sWLUlXVLJk++eQTCUAuXrzYYPuYMWMkALlt27Ys5+SUd+/evRKALF68uIyOjtZvT0tLk+3bt5cA5MSJEw2uU6JECQlAtmnTRiYlJem3//fff9LT01N6enrK1NTUbDM86cqVKzI9PT3L9lmzZkkA8osvvjDYPnfu3Bw/UymNfw8mT55ssP3BgwfyxRdflEIIeezYMf32bdu26T+fhQsXGpwzcOBACUB6e3vLCRMmGOwbP358tp935vP7+vrKK1eu6LdnZGTIrl27SgBy/PjxBueUKFFClihRItvXk3nO8ePHs+yLjY3N9pwnPf4af/jhB4N9me0zJCREZmRk6Ldnvs/Dhw83+PzS09P178uKFSv02zN/LwDIjz76yKhcmTI/cyGEXLduXZb9Of3OZWrcuLF88j8HH3/NT7aln376SQKQb7755nPlJCIqyHh7ORFRATVkyBAAWSdUi46Oxtq1a+Hv749OnTrpt5cpUybLNRRFwYgRIwAAGzZsMCnHv//+i02bNqFUqVIYPny4wb5OnTqhcePG2Z5XunTpLLfFArpe0tzkedycOXMAAJ988gkCAgL02+3t7TFlyhQoipLjhHTff/89XFxc9I8z38/4+HicP3/eqOcvUaKEvrf+cQMHDkShQoXM8hoz3b59GwsXLkTNmjUxatQog33Ozs76W+MXLVqU5dzw8HD06dPHYFu/fv0AAJ6envjwww8N9vXt2xcAcPz48WyzjBgxAiVKlNA/VhQFX331FRRF0X8mz+PxzyGTn5/fc10jJCQEQ4cONdiW2T6joqKwa9cuAICqqvjhhx8QEBCAb775xuDzs7Ozw5QpUyCEwG+//ZblOYoUKfJcve9PZmndurVJ5+akQYMGWW5LHzhwIOzt7XHw4EGzPhcRkS3j7eVERAVUs2bNUKZMGezZswdnz55F+fLlAQBz585Feno6+vfvbzCp2O3bt/HVV19h7dq1uHTpEhITEw2ud/36dZNyHDt2DICucMuuwGzSpAl27NiRZXtiYiK+++47LF++HJGRkbh3757BeGlT8zwuc6K5Zs2aZdkXGhqKoKAgXL58GfHx8fD09NTv8/T0REhISJZzihcvDgBZxmPnJC0t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lNdHAY5OwNkTkOv/1DoSERERkVVi0U1ERNkSgcUher8OAJArf4OMOqtxIiIiIiLrw6KbiIhyJOo3h6jdGFBV3TJiife0jkRERERkVVh0ExFRjoQQEK++CfgHAndiof76A6SUWsciIiIishosuomI6KmEsyuUIe8DdvbAsf2Q29dqHYmIiIjIarDoJiKiZxIlQiC69wMAyKWzIa9d0jgRERERkXVg0U1EREYRzTsClWsB6elQf/kKMvmB1pGIiIiI8j0W3UREZBQhBJT+IwAvX+C/65CLftY6EhEREVG+x6KbiIiMJjwKQRn8P0AokPu2Qt23TetIRERERPkai24iInouIrQiRIdeAAD52wzIm9c1TkRERESUf7HoJiKi5ybavQSUqwSkJEP95UvItDStIxERERHlSyy6iYjouQnFDsqgdwH3QsA/lyGXzdU6EhEREVG+xKKbiIhMIrx8oQx8BwAgt66BPL5f20BERERE+RCLbiIiMpmoVBOiVWcAgDr3e8jbsdoGIiIiIspnWHQTEVGuiC6vAiXLAkn3oc76GjIjQ+tIRERERPkGi24iIsoVYe8AZcj7gIsrEHUWctXvWkciIiIiyjdYdBMRUa6JwgEQrw4DAMh1f0CePaFxIiIiIqL8gUU3ERGZhVKrIUTDVoCUUGdPhUyI0zoSERERkeZYdBMRkdmInoOBosFA/F2oc76BVFWtIxERERFpyqSi+8GDB7h165bBtjt37mDJkiVYuHAhoqKizBKOiIisi3BygjJkFODoCJw+BrlphdaRiIiIiDRlUtH9888/45tvvtE/TkpKwscff4yIiAisWbMGY8aMwenTp80WkoiIrIcoFqzr8QYgly+AvHhO40RERERE2jGp6D5//jyqV6+uf7xr1y7cvXsXn3/+OebOnYvg4GBERESYLSQREVkX0bAVRK2GQEYG1JlfQybd1zoSERERkSZMKroTEhLg4+Ojf3z48GGEhYUhNDQULi4uaNy4Ma5cuWKujEREZGWEEBCvDAX8igC3Y6DOnwYppdaxiIiIiPKcSUW3m5sb4uLiAACpqak4d+4cKleu/OiiioLU1FSzBCQiIuskXN1063fb2QFH9kLu3KB1JCIiIqI8Z1LRHRoaio0bN+LgwYOYN28eUlNTUatWLf3+6Ohog55wIiIqmESpUIiufQEAcsksyH+vaBuIiIiI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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "if not logs_df.empty and {\"action\", \"task_name\", \"run_id\"}.issubset(logs_df.columns):\n", + " attempt_logs = logs_df[logs_df[\"action\"] == \"validate_attempt\"].copy()\n", + "\n", + " if not attempt_logs.empty:\n", + " attempts_per_run = (\n", + " attempt_logs\n", + " .groupby([\"task_name\", \"run_id\"], dropna=False)\n", + " .size()\n", + " .reset_index(name=\"attempt_count\")\n", + " .sort_values([\"task_name\", \"run_id\"])\n", + " )\n", + "\n", + " display(attempts_per_run.head(30))\n", + "\n", + " fig, ax = plt.subplots(figsize=(10, 4))\n", + " for task_name, g in attempts_per_run.groupby(\"task_name\"):\n", + " ax.plot(g[\"run_id\"], g[\"attempt_count\"], marker=\"o\", label=task_name)\n", + "\n", + " ax.set_title(\"Validation attempts per run\")\n", + " ax.set_xlabel(\"Run ID\")\n", + " ax.set_ylabel(\"Validation attempts\")\n", + " ax.legend()\n", + " plt.tight_layout()\n", + " else:\n", + " print(\"No validation attempt logs found.\")\n", + "else:\n", + " print(\"No logs available for attempt trend analysis.\")" + ] + }, + { + "cell_type": "markdown", + "id": "a28fd392", + "metadata": {}, + "source": [ + "## User action endpoint activity" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "6f82ba83", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
task_nametargetcount
10Coding TaskNaN35
0Coding TaskGET /9
5Coding TaskGET /status/modal5
3Coding TaskGET /issues/pm-2vr/edit2
6Coding TaskPATCH /issues/pm-2vr2
1Coding TaskGET /issues/create1
2Coding TaskGET /issues/pm-2vr/assignee1
4Coding TaskGET /issues/pm-c9p/assignee1
7Coding TaskPATCH /issues/pm-2vr/assignee1
8Coding TaskPATCH /issues/pm-c9p/assignee1
9Coding TaskPOST /issues1
\n", + "
" + ], + "text/plain": [ + " task_name target count\n", + "10 Coding Task NaN 35\n", + "0 Coding Task GET / 9\n", + "5 Coding Task GET /status/modal 5\n", + "3 Coding Task GET /issues/pm-2vr/edit 2\n", + "6 Coding Task PATCH /issues/pm-2vr 2\n", + "1 Coding Task GET /issues/create 1\n", + "2 Coding Task GET /issues/pm-2vr/assignee 1\n", + "4 Coding Task GET /issues/pm-c9p/assignee 1\n", + "7 Coding Task PATCH /issues/pm-2vr/assignee 1\n", + "8 Coding Task PATCH /issues/pm-c9p/assignee 1\n", + "9 Coding Task POST /issues 1" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "if not logs_df.empty and {\"level\", \"task_name\", \"target\"}.issubset(logs_df.columns):\n", + " user_actions = logs_df[logs_df[\"level\"] == \"user_action\"].copy()\n", + "\n", + " if not user_actions.empty:\n", + " endpoint_counts = (\n", + " user_actions\n", + " .groupby([\"task_name\", \"target\"], dropna=False)\n", + " .size()\n", + " .reset_index(name=\"count\")\n", + " .sort_values([\"task_name\", \"count\"], ascending=[True, False])\n", + " )\n", + "\n", + " display(endpoint_counts.groupby(\"task_name\", dropna=False).head(15))\n", + " else:\n", + " print(\"No user_action logs found.\")\n", + "else:\n", + " print(\"No logs available for endpoint activity analysis.\")" + ] + }, + { + "cell_type": "markdown", + "id": "32a64572", + "metadata": {}, + "source": [ + "## Quick filters for ad-hoc debugging" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "f8502e4a", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Task: Coding Task, run_id: 1\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
run_idtask_nameinterface_typestarted_atended_atduration_mscompletedvalidation_attemptsvalidation_successesvalidation_failuresvalidation_checks_passedvalidation_checks_failedquestionnaire_completedquestionnaire_user_quitquestionnaire_answerslogsfileerror
01Coding Taskweb2026-03-04 15:11:56.497145550+01:002026-03-04 15:13:26.641005636+01:0090143True84183499242TrueFalse{'final_confirmation': None, 'task_completed':...[{'timestamp': '2026-03-04T15:11:56.497159847+...coding-task-stats.jsonNaN
\n", + "
" + ], + "text/plain": [ + " run_id task_name interface_type started_at \\\n", + "0 1 Coding Task web 2026-03-04 15:11:56.497145550+01:00 \n", + "\n", + " ended_at duration_ms completed \\\n", + "0 2026-03-04 15:13:26.641005636+01:00 90143 True \n", + "\n", + " validation_attempts validation_successes validation_failures \\\n", + "0 84 1 83 \n", + "\n", + " validation_checks_passed validation_checks_failed \\\n", + "0 499 242 \n", + "\n", + " questionnaire_completed questionnaire_user_quit \\\n", + "0 True False \n", + "\n", + " questionnaire_answers \\\n", + "0 {'final_confirmation': None, 'task_completed':... \n", + "\n", + " logs file \\\n", + "0 [{'timestamp': '2026-03-04T15:11:56.497159847+... coding-task-stats.json \n", + "\n", + " error \n", + "0 NaN " + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
timestamplevelmessagesourceactionresulttask_namefilerun_idrun_completedtargetattempt
02026-03-04 15:11:56.497159847+01:00infotask run startedsystemtask_run_startedokCoding Taskcoding-task-stats.json1TrueNaNNaN
12026-03-04 15:11:57.463612409+01:00user_action{\"source\":\"web\",\"action\":\"request\",\"target\":\"G...webrequestokCoding Taskcoding-task-stats.json1TrueGET /NaN
22026-03-04 15:11:58.318154048+01:00validationvalidation attempt 1systemvalidate_attemptfailedCoding Taskcoding-task-stats.json1TrueNaN1.0
32026-03-04 15:11:58.318199703+01:00validationvalidation check: The original issue should be...systemvalidate_checkfailedCoding Taskcoding-task-stats.json1TrueThe original issue should be assigned to 'Me'....1.0
42026-03-04 15:11:58.318212096+01:00validationvalidation check: No new issues created. Pleas...systemvalidate_checkfailedCoding Taskcoding-task-stats.json1TrueNo new issues created. Please create an issue ...1.0
52026-03-04 15:11:58.318221323+01:00validationvalidation check: No new issues createdsystemvalidate_checkfailedCoding Taskcoding-task-stats.json1TrueNo new issues created1.0
62026-03-04 15:11:59.317079434+01:00validationvalidation attempt 2systemvalidate_attemptfailedCoding Taskcoding-task-stats.json1TrueNaN2.0
72026-03-04 15:11:59.317145086+01:00validationvalidation check: The original issue should be...systemvalidate_checkfailedCoding Taskcoding-task-stats.json1TrueThe original issue should be assigned to 'Me'....2.0
82026-03-04 15:11:59.317171636+01:00validationvalidation check: No new issues created. Pleas...systemvalidate_checkfailedCoding Taskcoding-task-stats.json1TrueNo new issues created. Please create an issue ...2.0
92026-03-04 15:11:59.317194609+01:00validationvalidation check: No new issues createdsystemvalidate_checkfailedCoding Taskcoding-task-stats.json1TrueNo new issues created2.0
102026-03-04 15:12:00.317912946+01:00validationvalidation attempt 3systemvalidate_attemptfailedCoding Taskcoding-task-stats.json1TrueNaN3.0
112026-03-04 15:12:00.317988177+01:00validationvalidation check: The original issue should be...systemvalidate_checkfailedCoding Taskcoding-task-stats.json1TrueThe original issue should be assigned to 'Me'....3.0
122026-03-04 15:12:00.318006912+01:00validationvalidation check: No new issues created. Pleas...systemvalidate_checkfailedCoding Taskcoding-task-stats.json1TrueNo new issues created. Please create an issue ...3.0
132026-03-04 15:12:00.318020798+01:00validationvalidation check: No new issues createdsystemvalidate_checkfailedCoding Taskcoding-task-stats.json1TrueNo new issues created3.0
142026-03-04 15:12:01.317868206+01:00validationvalidation attempt 4systemvalidate_attemptfailedCoding Taskcoding-task-stats.json1TrueNaN4.0
152026-03-04 15:12:01.317945661+01:00validationvalidation check: The original issue should be...systemvalidate_checkfailedCoding Taskcoding-task-stats.json1TrueThe original issue should be assigned to 'Me'....4.0
162026-03-04 15:12:01.317963825+01:00validationvalidation check: No new issues created. Pleas...systemvalidate_checkfailedCoding Taskcoding-task-stats.json1TrueNo new issues created. Please create an issue ...4.0
172026-03-04 15:12:01.317977450+01:00validationvalidation check: No new issues createdsystemvalidate_checkfailedCoding Taskcoding-task-stats.json1TrueNo new issues created4.0
182026-03-04 15:12:01.570644963+01:00user_action{\"source\":\"web\",\"action\":\"request\",\"target\":\"G...webrequestokCoding Taskcoding-task-stats.json1TrueGET /issues/createNaN
192026-03-04 15:12:02.317463062+01:00validationvalidation attempt 5systemvalidate_attemptfailedCoding Taskcoding-task-stats.json1TrueNaN5.0
202026-03-04 15:12:02.317511663+01:00validationvalidation check: The original issue should be...systemvalidate_checkfailedCoding Taskcoding-task-stats.json1TrueThe original issue should be assigned to 'Me'....5.0
212026-03-04 15:12:02.317527422+01:00validationvalidation check: No new issues created. Pleas...systemvalidate_checkfailedCoding Taskcoding-task-stats.json1TrueNo new issues created. Please create an issue ...5.0
222026-03-04 15:12:02.317538603+01:00validationvalidation check: No new issues createdsystemvalidate_checkfailedCoding Taskcoding-task-stats.json1TrueNo new issues created5.0
232026-03-04 15:12:03.317011327+01:00validationvalidation attempt 6systemvalidate_attemptfailedCoding Taskcoding-task-stats.json1TrueNaN6.0
242026-03-04 15:12:03.317060369+01:00validationvalidation check: The original issue should be...systemvalidate_checkfailedCoding Taskcoding-task-stats.json1TrueThe original issue should be assigned to 'Me'....6.0
252026-03-04 15:12:03.317072492+01:00validationvalidation check: No new issues created. Pleas...systemvalidate_checkfailedCoding Taskcoding-task-stats.json1TrueNo new issues created. Please create an issue ...6.0
262026-03-04 15:12:03.317081078+01:00validationvalidation check: No new issues createdsystemvalidate_checkfailedCoding Taskcoding-task-stats.json1TrueNo new issues created6.0
272026-03-04 15:12:04.317413612+01:00validationvalidation attempt 7systemvalidate_attemptfailedCoding Taskcoding-task-stats.json1TrueNaN7.0
282026-03-04 15:12:04.317467062+01:00validationvalidation check: The original issue should be...systemvalidate_checkfailedCoding Taskcoding-task-stats.json1TrueThe original issue should be assigned to 'Me'....7.0
292026-03-04 15:12:04.317479285+01:00validationvalidation check: No new issues created. Pleas...systemvalidate_checkfailedCoding Taskcoding-task-stats.json1TrueNo new issues created. Please create an issue ...7.0
302026-03-04 15:12:04.317488011+01:00validationvalidation check: No new issues createdsystemvalidate_checkfailedCoding Taskcoding-task-stats.json1TrueNo new issues created7.0
312026-03-04 15:12:05.317465258+01:00validationvalidation attempt 8systemvalidate_attemptfailedCoding Taskcoding-task-stats.json1TrueNaN8.0
322026-03-04 15:12:05.317554294+01:00validationvalidation check: The original issue should be...systemvalidate_checkfailedCoding Taskcoding-task-stats.json1TrueThe original issue should be assigned to 'Me'....8.0
332026-03-04 15:12:05.317579081+01:00validationvalidation check: No new issues created. Pleas...systemvalidate_checkfailedCoding Taskcoding-task-stats.json1TrueNo new issues created. Please create an issue ...8.0
342026-03-04 15:12:05.317597465+01:00validationvalidation check: No new issues createdsystemvalidate_checkfailedCoding Taskcoding-task-stats.json1TrueNo new issues created8.0
352026-03-04 15:12:06.317295761+01:00validationvalidation attempt 9systemvalidate_attemptfailedCoding Taskcoding-task-stats.json1TrueNaN9.0
362026-03-04 15:12:06.317375340+01:00validationvalidation check: The original issue should be...systemvalidate_checkfailedCoding Taskcoding-task-stats.json1TrueThe original issue should be assigned to 'Me'....9.0
372026-03-04 15:12:06.317405787+01:00validationvalidation check: No new issues created. Pleas...systemvalidate_checkfailedCoding Taskcoding-task-stats.json1TrueNo new issues created. Please create an issue ...9.0
382026-03-04 15:12:06.317434641+01:00validationvalidation check: No new issues createdsystemvalidate_checkfailedCoding Taskcoding-task-stats.json1TrueNo new issues created9.0
392026-03-04 15:12:07.317542913+01:00validationvalidation attempt 10systemvalidate_attemptfailedCoding Taskcoding-task-stats.json1TrueNaN10.0
402026-03-04 15:12:07.317613084+01:00validationvalidation check: The original issue should be...systemvalidate_checkfailedCoding Taskcoding-task-stats.json1TrueThe original issue should be assigned to 'Me'....10.0
412026-03-04 15:12:07.317626178+01:00validationvalidation check: No new issues created. Pleas...systemvalidate_checkfailedCoding Taskcoding-task-stats.json1TrueNo new issues created. Please create an issue ...10.0
422026-03-04 15:12:07.317635035+01:00validationvalidation check: No new issues createdsystemvalidate_checkfailedCoding Taskcoding-task-stats.json1TrueNo new issues created10.0
432026-03-04 15:12:08.317693562+01:00validationvalidation attempt 11systemvalidate_attemptfailedCoding Taskcoding-task-stats.json1TrueNaN11.0
442026-03-04 15:12:08.317760387+01:00validationvalidation check: The original issue should be...systemvalidate_checkfailedCoding Taskcoding-task-stats.json1TrueThe original issue should be assigned to 'Me'....11.0
452026-03-04 15:12:08.317773101+01:00validationvalidation check: No new issues created. Pleas...systemvalidate_checkfailedCoding Taskcoding-task-stats.json1TrueNo new issues created. Please create an issue ...11.0
462026-03-04 15:12:08.317782208+01:00validationvalidation check: No new issues createdsystemvalidate_checkfailedCoding Taskcoding-task-stats.json1TrueNo new issues created11.0
472026-03-04 15:12:09.317310231+01:00validationvalidation attempt 12systemvalidate_attemptfailedCoding Taskcoding-task-stats.json1TrueNaN12.0
482026-03-04 15:12:09.317473836+01:00validationvalidation check: The original issue should be...systemvalidate_checkfailedCoding Taskcoding-task-stats.json1TrueThe original issue should be assigned to 'Me'....12.0
492026-03-04 15:12:09.317512218+01:00validationvalidation check: No new issues created. Pleas...systemvalidate_checkfailedCoding Taskcoding-task-stats.json1TrueNo new issues created. Please create an issue ...12.0
\n", + "
" + ], + "text/plain": [ + " timestamp level \\\n", + "0 2026-03-04 15:11:56.497159847+01:00 info \n", + "1 2026-03-04 15:11:57.463612409+01:00 user_action \n", + "2 2026-03-04 15:11:58.318154048+01:00 validation \n", + "3 2026-03-04 15:11:58.318199703+01:00 validation \n", + "4 2026-03-04 15:11:58.318212096+01:00 validation \n", + "5 2026-03-04 15:11:58.318221323+01:00 validation \n", + "6 2026-03-04 15:11:59.317079434+01:00 validation \n", + "7 2026-03-04 15:11:59.317145086+01:00 validation \n", + "8 2026-03-04 15:11:59.317171636+01:00 validation \n", + "9 2026-03-04 15:11:59.317194609+01:00 validation \n", + "10 2026-03-04 15:12:00.317912946+01:00 validation \n", + "11 2026-03-04 15:12:00.317988177+01:00 validation \n", + "12 2026-03-04 15:12:00.318006912+01:00 validation \n", + "13 2026-03-04 15:12:00.318020798+01:00 validation \n", + "14 2026-03-04 15:12:01.317868206+01:00 validation \n", + "15 2026-03-04 15:12:01.317945661+01:00 validation \n", + "16 2026-03-04 15:12:01.317963825+01:00 validation \n", + "17 2026-03-04 15:12:01.317977450+01:00 validation \n", + "18 2026-03-04 15:12:01.570644963+01:00 user_action \n", + "19 2026-03-04 15:12:02.317463062+01:00 validation \n", + "20 2026-03-04 15:12:02.317511663+01:00 validation \n", + "21 2026-03-04 15:12:02.317527422+01:00 validation \n", + "22 2026-03-04 15:12:02.317538603+01:00 validation \n", + "23 2026-03-04 15:12:03.317011327+01:00 validation \n", + "24 2026-03-04 15:12:03.317060369+01:00 validation \n", + "25 2026-03-04 15:12:03.317072492+01:00 validation \n", + "26 2026-03-04 15:12:03.317081078+01:00 validation \n", + "27 2026-03-04 15:12:04.317413612+01:00 validation \n", + "28 2026-03-04 15:12:04.317467062+01:00 validation \n", + "29 2026-03-04 15:12:04.317479285+01:00 validation \n", + "30 2026-03-04 15:12:04.317488011+01:00 validation \n", + "31 2026-03-04 15:12:05.317465258+01:00 validation \n", + "32 2026-03-04 15:12:05.317554294+01:00 validation \n", + "33 2026-03-04 15:12:05.317579081+01:00 validation \n", + "34 2026-03-04 15:12:05.317597465+01:00 validation \n", + "35 2026-03-04 15:12:06.317295761+01:00 validation \n", + "36 2026-03-04 15:12:06.317375340+01:00 validation \n", + "37 2026-03-04 15:12:06.317405787+01:00 validation \n", + "38 2026-03-04 15:12:06.317434641+01:00 validation \n", + "39 2026-03-04 15:12:07.317542913+01:00 validation \n", + "40 2026-03-04 15:12:07.317613084+01:00 validation \n", + "41 2026-03-04 15:12:07.317626178+01:00 validation \n", + "42 2026-03-04 15:12:07.317635035+01:00 validation \n", + "43 2026-03-04 15:12:08.317693562+01:00 validation \n", + "44 2026-03-04 15:12:08.317760387+01:00 validation \n", + "45 2026-03-04 15:12:08.317773101+01:00 validation \n", + "46 2026-03-04 15:12:08.317782208+01:00 validation \n", + "47 2026-03-04 15:12:09.317310231+01:00 validation \n", + "48 2026-03-04 15:12:09.317473836+01:00 validation \n", + "49 2026-03-04 15:12:09.317512218+01:00 validation \n", + "\n", + " message source \\\n", + "0 task run started system \n", + "1 {\"source\":\"web\",\"action\":\"request\",\"target\":\"G... web \n", + "2 validation attempt 1 system \n", + "3 validation check: The original issue should be... system \n", + "4 validation check: No new issues created. Pleas... system \n", + "5 validation check: No new issues created system \n", + "6 validation attempt 2 system \n", + "7 validation check: The original issue should be... system \n", + "8 validation check: No new issues created. Pleas... system \n", + "9 validation check: No new issues created system \n", + "10 validation attempt 3 system \n", + "11 validation check: The original issue should be... system \n", + "12 validation check: No new issues created. Pleas... system \n", + "13 validation check: No new issues created system \n", + "14 validation attempt 4 system \n", + "15 validation check: The original issue should be... system \n", + "16 validation check: No new issues created. Pleas... system \n", + "17 validation check: No new issues created system \n", + "18 {\"source\":\"web\",\"action\":\"request\",\"target\":\"G... web \n", + "19 validation attempt 5 system \n", + "20 validation check: The original issue should be... system \n", + "21 validation check: No new issues created. Pleas... system \n", + "22 validation check: No new issues created system \n", + "23 validation attempt 6 system \n", + "24 validation check: The original issue should be... system \n", + "25 validation check: No new issues created. Pleas... system \n", + "26 validation check: No new issues created system \n", + "27 validation attempt 7 system \n", + "28 validation check: The original issue should be... system \n", + "29 validation check: No new issues created. Pleas... system \n", + "30 validation check: No new issues created system \n", + "31 validation attempt 8 system \n", + "32 validation check: The original issue should be... system \n", + "33 validation check: No new issues created. Pleas... system \n", + "34 validation check: No new issues created system \n", + "35 validation attempt 9 system \n", + "36 validation check: The original issue should be... system \n", + "37 validation check: No new issues created. Pleas... system \n", + "38 validation check: No new issues created system \n", + "39 validation attempt 10 system \n", + "40 validation check: The original issue should be... system \n", + "41 validation check: No new issues created. Pleas... system \n", + "42 validation check: No new issues created system \n", + "43 validation attempt 11 system \n", + "44 validation check: The original issue should be... system \n", + "45 validation check: No new issues created. Pleas... system \n", + "46 validation check: No new issues created system \n", + "47 validation attempt 12 system \n", + "48 validation check: The original issue should be... system \n", + "49 validation check: No new issues created. Pleas... system \n", + "\n", + " action result task_name file run_id \\\n", + "0 task_run_started ok Coding Task coding-task-stats.json 1 \n", + "1 request ok Coding Task coding-task-stats.json 1 \n", + "2 validate_attempt failed Coding Task coding-task-stats.json 1 \n", + "3 validate_check failed Coding Task coding-task-stats.json 1 \n", + "4 validate_check failed Coding Task coding-task-stats.json 1 \n", + "5 validate_check failed Coding Task coding-task-stats.json 1 \n", + "6 validate_attempt failed Coding Task coding-task-stats.json 1 \n", + "7 validate_check failed Coding Task coding-task-stats.json 1 \n", + "8 validate_check failed Coding Task coding-task-stats.json 1 \n", + "9 validate_check failed Coding Task coding-task-stats.json 1 \n", + "10 validate_attempt failed Coding Task coding-task-stats.json 1 \n", + "11 validate_check failed Coding Task coding-task-stats.json 1 \n", + "12 validate_check failed Coding Task coding-task-stats.json 1 \n", + "13 validate_check failed Coding Task coding-task-stats.json 1 \n", + "14 validate_attempt failed Coding Task coding-task-stats.json 1 \n", + "15 validate_check failed Coding Task coding-task-stats.json 1 \n", + "16 validate_check failed Coding Task coding-task-stats.json 1 \n", + "17 validate_check failed Coding Task coding-task-stats.json 1 \n", + "18 request ok Coding Task coding-task-stats.json 1 \n", + "19 validate_attempt failed Coding Task coding-task-stats.json 1 \n", + "20 validate_check failed Coding Task coding-task-stats.json 1 \n", + "21 validate_check failed Coding Task coding-task-stats.json 1 \n", + "22 validate_check failed Coding Task coding-task-stats.json 1 \n", + "23 validate_attempt failed Coding Task coding-task-stats.json 1 \n", + "24 validate_check failed Coding Task coding-task-stats.json 1 \n", + "25 validate_check failed Coding Task coding-task-stats.json 1 \n", + "26 validate_check failed Coding Task coding-task-stats.json 1 \n", + "27 validate_attempt failed Coding Task coding-task-stats.json 1 \n", + "28 validate_check failed Coding Task coding-task-stats.json 1 \n", + "29 validate_check failed Coding Task coding-task-stats.json 1 \n", + "30 validate_check failed Coding Task coding-task-stats.json 1 \n", + "31 validate_attempt failed Coding Task coding-task-stats.json 1 \n", + "32 validate_check failed Coding Task coding-task-stats.json 1 \n", + "33 validate_check failed Coding Task coding-task-stats.json 1 \n", + "34 validate_check failed Coding Task coding-task-stats.json 1 \n", + "35 validate_attempt failed Coding Task coding-task-stats.json 1 \n", + "36 validate_check failed Coding Task coding-task-stats.json 1 \n", + "37 validate_check failed Coding Task coding-task-stats.json 1 \n", + "38 validate_check failed Coding Task coding-task-stats.json 1 \n", + "39 validate_attempt failed Coding Task coding-task-stats.json 1 \n", + "40 validate_check failed Coding Task coding-task-stats.json 1 \n", + "41 validate_check failed Coding Task coding-task-stats.json 1 \n", + "42 validate_check failed Coding Task coding-task-stats.json 1 \n", + "43 validate_attempt failed Coding Task coding-task-stats.json 1 \n", + "44 validate_check failed Coding Task coding-task-stats.json 1 \n", + "45 validate_check failed Coding Task coding-task-stats.json 1 \n", + "46 validate_check failed Coding Task coding-task-stats.json 1 \n", + "47 validate_attempt failed Coding Task coding-task-stats.json 1 \n", + "48 validate_check failed Coding Task coding-task-stats.json 1 \n", + "49 validate_check failed Coding Task coding-task-stats.json 1 \n", + "\n", + " run_completed target attempt \n", + "0 True NaN NaN \n", + "1 True GET / NaN \n", + "2 True NaN 1.0 \n", + "3 True The original issue should be assigned to 'Me'.... 1.0 \n", + "4 True No new issues created. Please create an issue ... 1.0 \n", + "5 True No new issues created 1.0 \n", + "6 True NaN 2.0 \n", + "7 True The original issue should be assigned to 'Me'.... 2.0 \n", + "8 True No new issues created. Please create an issue ... 2.0 \n", + "9 True No new issues created 2.0 \n", + "10 True NaN 3.0 \n", + "11 True The original issue should be assigned to 'Me'.... 3.0 \n", + "12 True No new issues created. Please create an issue ... 3.0 \n", + "13 True No new issues created 3.0 \n", + "14 True NaN 4.0 \n", + "15 True The original issue should be assigned to 'Me'.... 4.0 \n", + "16 True No new issues created. Please create an issue ... 4.0 \n", + "17 True No new issues created 4.0 \n", + "18 True GET /issues/create NaN \n", + "19 True NaN 5.0 \n", + "20 True The original issue should be assigned to 'Me'.... 5.0 \n", + "21 True No new issues created. Please create an issue ... 5.0 \n", + "22 True No new issues created 5.0 \n", + "23 True NaN 6.0 \n", + "24 True The original issue should be assigned to 'Me'.... 6.0 \n", + "25 True No new issues created. Please create an issue ... 6.0 \n", + "26 True No new issues created 6.0 \n", + "27 True NaN 7.0 \n", + "28 True The original issue should be assigned to 'Me'.... 7.0 \n", + "29 True No new issues created. Please create an issue ... 7.0 \n", + "30 True No new issues created 7.0 \n", + "31 True NaN 8.0 \n", + "32 True The original issue should be assigned to 'Me'.... 8.0 \n", + "33 True No new issues created. Please create an issue ... 8.0 \n", + "34 True No new issues created 8.0 \n", + "35 True NaN 9.0 \n", + "36 True The original issue should be assigned to 'Me'.... 9.0 \n", + "37 True No new issues created. Please create an issue ... 9.0 \n", + "38 True No new issues created 9.0 \n", + "39 True NaN 10.0 \n", + "40 True The original issue should be assigned to 'Me'.... 10.0 \n", + "41 True No new issues created. Please create an issue ... 10.0 \n", + "42 True No new issues created 10.0 \n", + "43 True NaN 11.0 \n", + "44 True The original issue should be assigned to 'Me'.... 11.0 \n", + "45 True No new issues created. Please create an issue ... 11.0 \n", + "46 True No new issues created 11.0 \n", + "47 True NaN 12.0 \n", + "48 True The original issue should be assigned to 'Me'.... 12.0 \n", + "49 True No new issues created. Please create an issue ... 12.0 " + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Example: inspect one task and one run.\n", + "if not runs_df.empty and {\"task_name\", \"run_id\"}.issubset(runs_df.columns):\n", + " task = runs_df[\"task_name\"].iloc[0]\n", + " run_id = 1\n", + "\n", + " print(f\"Task: {task}, run_id: {run_id}\")\n", + " display(runs_df[(runs_df[\"task_name\"] == task) & (runs_df[\"run_id\"] == run_id)])\n", + "\n", + " if not logs_df.empty and {\"task_name\", \"run_id\"}.issubset(logs_df.columns):\n", + " display(logs_df[(logs_df[\"task_name\"] == task) & (logs_df[\"run_id\"] == run_id)].head(50))\n", + "else:\n", + " print(\"No run data available.\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e6a94bb5", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.14.3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} From 0f82146fe5c79510ad8c9a5c40c931e986eb3f5e Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Wed, 4 Mar 2026 20:46:55 +0100 Subject: [PATCH 39/51] logg all checks and show if done or not --- pkg/web/handler/task.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pkg/web/handler/task.go b/pkg/web/handler/task.go index a830839..6b10a71 100644 --- a/pkg/web/handler/task.go +++ b/pkg/web/handler/task.go @@ -46,8 +46,11 @@ func feedbackList(feedback ValidationFeedback) templ.Component { return templ.ComponentFunc(func(ctx context.Context, w io.Writer) error { for _, check := range feedback.Checks { if !check.Valid { - io.WriteString(w, `

`+check.Message+`

`) + io.WriteString(w, `

`+"❌ "+check.Message+`

`) + } else { + io.WriteString(w, `

`+"✅ "+check.Message+`

`) } + } return nil }) From 5a9a8238d950b402fadd530732ab7cec73e6b71a Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Wed, 4 Mar 2026 20:50:45 +0100 Subject: [PATCH 40/51] remove unused dep --- go.mod | 1 - go.sum | 2 -- 2 files changed, 3 deletions(-) diff --git a/go.mod b/go.mod index 54094f7..ff1e7b2 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,6 @@ require ( github.com/muesli/reflow v0.3.0 github.com/steveyegge/beads v0.49.6 go.mongodb.org/mongo-driver v1.17.9 - go.mongodb.org/mongo-driver/v2 v2.5.0 ) // Terminal dependencies diff --git a/go.sum b/go.sum index 8bdd730..9e37a12 100644 --- a/go.sum +++ b/go.sum @@ -256,8 +256,6 @@ github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfS github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.mongodb.org/mongo-driver v1.17.9 h1:IexDdCuuNJ3BHrELgBlyaH9p60JXAvdzWR128q+U5tU= go.mongodb.org/mongo-driver v1.17.9/go.mod h1:LlOhpH5NUEfhxcAwG0UEkMqwYcc4JU18gtCdGudk/tQ= -go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE= -go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= From 3d183d7fcdd6a740c30f49c0217987a82a1027db Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Wed, 4 Mar 2026 22:43:28 +0100 Subject: [PATCH 41/51] change validation to be submission callback based --- internal/models/app.go | 2 ++ pkg/task/runner.go | 40 +++++++++++----------------------------- pkg/task/validation.go | 7 ++----- pkg/web/handler/task.go | 12 ++++++++++++ 4 files changed, 27 insertions(+), 34 deletions(-) diff --git a/internal/models/app.go b/internal/models/app.go index efeb815..5ba33b1 100644 --- a/internal/models/app.go +++ b/internal/models/app.go @@ -17,6 +17,8 @@ type App struct { CurrentFeedback *ValidationFeedback ActionLogger func(string) + + SubmitChan chan<- struct{} } func (a *App) LogAction(action string) { diff --git a/pkg/task/runner.go b/pkg/task/runner.go index 005a03f..3e25791 100644 --- a/pkg/task/runner.go +++ b/pkg/task/runner.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "log/slog" - "time" "github.com/LazyBachelor/LazyPM/internal/models" tea "github.com/charmbracelet/bubbletea" @@ -69,13 +68,19 @@ func (r *TaskRunner) Run(ctx context.Context, t Tasker, i Interface, iType Inter // Validation feedbackChan := make(chan ValidationFeedback, 10) quitChan := make(chan bool, 1) + submitChan := make(chan struct{}, 1) if validated, ok := i.(ValidatedInterface); ok { validated.SetChannels(feedbackChan, quitChan) + validated.SetSubmitChan(submitChan) + } + + if r.app != nil { + r.app.SubmitChan = submitChan } engine := &ValidationEngine{task: t} - doneChan, stopChan := engine.Start(ctx, func(feedback ValidationFeedback) { + doneChan, stopChan := engine.Start(ctx, submitChan, func(feedback ValidationFeedback) { collector.recordValidation(feedback) if feedback.Success { @@ -84,6 +89,10 @@ func (r *TaskRunner) Run(ctx context.Context, t Tasker, i Interface, iType Inter feedback.Message = "Task not completed!" } + if r.app != nil { + r.app.CurrentFeedback = &feedback + } + select { case feedbackChan <- feedback: default: @@ -195,30 +204,3 @@ func runQuestionnaire(t Tasker, iType InterfaceType, collector *taskRunCollector return nil } - -func startValidationLoop(ctx context.Context, t Tasker, feedbackChan chan ValidationFeedback, doneChan chan bool, quitChan chan bool) { - ticker := time.NewTicker(1 * time.Second) - defer ticker.Stop() - - for { - select { - case <-ticker.C: - feedback := t.Validate(ctx) - if feedback.Success { - if feedback.Message == "" { - feedback.Message = "Task completed successfully! Going back to the survey menu..." - } - feedbackChan <- feedback - time.Sleep(4 * time.Second) - doneChan <- true - return - } - feedback.Message = "Task not completed!" - feedbackChan <- feedback - case <-quitChan: - return - case <-ctx.Done(): - return - } - } -} diff --git a/pkg/task/validation.go b/pkg/task/validation.go index 170c670..f2957e3 100644 --- a/pkg/task/validation.go +++ b/pkg/task/validation.go @@ -9,17 +9,14 @@ type ValidationEngine struct { task Tasker } -func (v *ValidationEngine) Start(ctx context.Context, onFeedback func(ValidationFeedback)) (done <-chan struct{}, stop chan<- struct{}) { +func (v *ValidationEngine) Start(ctx context.Context, submitChan <-chan struct{}, onFeedback func(ValidationFeedback)) (done <-chan struct{}, stop chan<- struct{}) { doneChan := make(chan struct{}, 1) stopChan := make(chan struct{}, 1) go func() { - ticker := time.NewTicker(time.Second) - defer ticker.Stop() - for { select { - case <-ticker.C: + case <-submitChan: feedback := v.task.Validate(ctx) if onFeedback != nil { diff --git a/pkg/web/handler/task.go b/pkg/web/handler/task.go index 6b10a71..dab8086 100644 --- a/pkg/web/handler/task.go +++ b/pkg/web/handler/task.go @@ -13,12 +13,24 @@ import ( type ValidationFeedback = models.ValidationFeedback var taskFeedback ValidationFeedback +var submitChan chan<- struct{} func SetTaskFeedback(feedback ValidationFeedback) { taskFeedback = feedback } +func SetSubmitChan(ch chan<- struct{}) { + submitChan = ch +} + func HandleTaskStatus(w http.ResponseWriter, r *http.Request) { + if submitChan != nil { + select { + case submitChan <- struct{}{}: + default: + } + } + hx := HTMX(r) if hx.IsHxRequest() { hx.WriteString(`` + taskFeedback.Message + ``) From 4842d30aabf97e8d6dac180d802949afde826635 Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Wed, 4 Mar 2026 22:44:01 +0100 Subject: [PATCH 42/51] change repl and status command to use callback --- internal/commands/survey/status.go | 18 +++++++++++++++++- internal/models/task.go | 1 + pkg/repl/repl.go | 11 ++++++++++- 3 files changed, 28 insertions(+), 2 deletions(-) diff --git a/internal/commands/survey/status.go b/internal/commands/survey/status.go index 0e5a954..8df5ac0 100644 --- a/internal/commands/survey/status.go +++ b/internal/commands/survey/status.go @@ -1,6 +1,8 @@ package survey import ( + "time" + "github.com/LazyBachelor/LazyPM/internal/commands/issues" "github.com/spf13/cobra" ) @@ -15,11 +17,25 @@ var StatusCmd = &cobra.Command{ func runStatusCmd(cmd *cobra.Command, args []string) error { app := issues.AppFromContext(cmd.Context()) - if app == nil || app.CurrentFeedback == nil { + if app == nil { cmd.Println("No validation status available.") return nil } + if app.SubmitChan != nil { + select { + case app.SubmitChan <- struct{}{}: + default: + } + } + + time.Sleep(100 * time.Millisecond) + + if app.CurrentFeedback == nil { + cmd.Println("No validation status available yet.") + return nil + } + if app.CurrentFeedback.Message == "" { cmd.Println("No validation status available yet.") return nil diff --git a/internal/models/task.go b/internal/models/task.go index 9f0538b..88d385a 100644 --- a/internal/models/task.go +++ b/internal/models/task.go @@ -28,6 +28,7 @@ type Check struct { type ValidatedInterface interface { Interface SetChannels(feedbackChan chan ValidationFeedback, quitChan chan bool) + SetSubmitChan(submitChan chan<- struct{}) } type TaskDetails struct { diff --git a/pkg/repl/repl.go b/pkg/repl/repl.go index 607100d..f91d475 100644 --- a/pkg/repl/repl.go +++ b/pkg/repl/repl.go @@ -31,6 +31,7 @@ You can also run shell commands directly. Type 'exit' or 'quit' to leave.` type REPL struct { feedbackChan chan ValidationFeedback quitChan chan bool + submitChan chan<- struct{} app *App currentFeedback ValidationFeedback @@ -65,7 +66,11 @@ func (r *REPL) Run(ctx context.Context, config app.Config) error { // Store app reference for updating feedback r.app = app - fmt.Println(style.TitleStyle.Render(ReplTitle)) // Print REPL title. + if r.submitChan != nil { + r.app.SubmitChan = r.submitChan + } + + fmt.Println(style.TitleStyle.Render(ReplTitle)) // Start goroutine to watch for validation feedback and quit signals if r.feedbackChan != nil && r.quitChan != nil { @@ -151,6 +156,10 @@ func (r *REPL) SetChannels(feedbackChan chan task.ValidationFeedback, quitChan c r.quitChan = quitChan } +func (r *REPL) SetSubmitChan(submitChan chan<- struct{}) { + r.submitChan = submitChan +} + func (r *REPL) logAction(action string) { if r.app != nil { r.app.LogAction(models.EncodeActionEvent(models.ActionEvent{ From cc6c4ea7cb15a81e2aa6a9b98cd7371fe8a001a3 Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Wed, 4 Mar 2026 22:44:55 +0100 Subject: [PATCH 43/51] change tui and status command to use callback. add keybind to submit --- pkg/tui/tui.go | 7 ++++- pkg/tui/views/dashboard/help_bar.go | 19 +++++++------ pkg/tui/views/dashboard/keys.go | 43 +++++++++++++++++++---------- pkg/tui/views/dashboard/model.go | 4 ++- pkg/tui/views/views.go | 4 +-- 5 files changed, 49 insertions(+), 28 deletions(-) diff --git a/pkg/tui/tui.go b/pkg/tui/tui.go index 6227f27..92c3a87 100644 --- a/pkg/tui/tui.go +++ b/pkg/tui/tui.go @@ -15,6 +15,7 @@ type ValidationFeedback = models.ValidationFeedback type Tui struct { feedbackChan chan ValidationFeedback quitChan chan bool + submitChan chan<- struct{} } func New() *Tui { @@ -29,7 +30,7 @@ func (t *Tui) Run(ctx context.Context, config Config) error { defer cleanup() - p := tea.NewProgram(views.NewDashboardView(app, t.feedbackChan, t.quitChan), + p := tea.NewProgram(views.NewDashboardView(app, t.feedbackChan, t.quitChan, t.submitChan), tea.WithAltScreen(), tea.WithMouseAllMotion()) if t.quitChan != nil { @@ -50,3 +51,7 @@ func (t *Tui) SetChannels(feedbackChan chan ValidationFeedback, quitChan chan bo t.feedbackChan = feedbackChan t.quitChan = quitChan } + +func (t *Tui) SetSubmitChan(submitChan chan<- struct{}) { + t.submitChan = submitChan +} diff --git a/pkg/tui/views/dashboard/help_bar.go b/pkg/tui/views/dashboard/help_bar.go index d5b7891..f0bb51e 100644 --- a/pkg/tui/views/dashboard/help_bar.go +++ b/pkg/tui/views/dashboard/help_bar.go @@ -31,15 +31,16 @@ func (h HelpBar) View() string { func (h HelpBar) shortHelp() string { keys := []string{ - styles.HighlightKey("tab") + " switch", - styles.HighlightKey("↑/k") + " up", - styles.HighlightKey("↓/j") + " down", - styles.HighlightKey("pgup/pgdn") + " page", - styles.HighlightKey("a") + " add", - styles.HighlightKey("e/d/s/p/t/c") + " edit", - styles.HighlightKey("x") + " delete", - styles.HighlightKey("q") + " quit", - styles.HighlightKey("?") + " help", + styles.HighlightKey("tab") + "switch ", + styles.HighlightKey("↑/k") + "up ", + styles.HighlightKey("↓/j") + "down ", + styles.HighlightKey("pgup/pgdn") + "page ", + styles.HighlightKey("a") + "add ", + styles.HighlightKey("e/d/s/p/t/c") + "edit ", + styles.HighlightKey("x") + "delete ", + styles.HighlightKey("q") + "quit ", + styles.HighlightKey("S") + "submit ", + styles.HighlightKey("?") + "help ", } content := lipgloss.JoinHorizontal(lipgloss.Left, keys...) return lipgloss.NewStyle(). diff --git a/pkg/tui/views/dashboard/keys.go b/pkg/tui/views/dashboard/keys.go index ac02aa8..dd0981b 100644 --- a/pkg/tui/views/dashboard/keys.go +++ b/pkg/tui/views/dashboard/keys.go @@ -6,21 +6,22 @@ import ( ) type DashboardKeyMap struct { - Help key.Binding - Quit key.Binding - SelectIssue key.Binding - BackToList key.Binding - ScrollUp key.Binding - ScrollDown key.Binding - SwitchWindow key.Binding - EditTitle key.Binding - EditDescription key.Binding - ChangeStatus key.Binding - ChangePriority key.Binding - ChangeType key.Binding - AddComment key.Binding - AddIssue key.Binding - DeleteIssue key.Binding + Help key.Binding + Quit key.Binding + SelectIssue key.Binding + BackToList key.Binding + ScrollUp key.Binding + ScrollDown key.Binding + SwitchWindow key.Binding + EditTitle key.Binding + EditDescription key.Binding + ChangeStatus key.Binding + ChangePriority key.Binding + ChangeType key.Binding + AddComment key.Binding + AddIssue key.Binding + DeleteIssue key.Binding + SubmitValidation key.Binding } var defaultDashboardKeyMap = DashboardKeyMap{ @@ -84,6 +85,10 @@ var defaultDashboardKeyMap = DashboardKeyMap{ key.WithKeys("x"), key.WithHelp("x", "delete issue"), ), + SubmitValidation: key.NewBinding( + key.WithKeys("S"), + key.WithHelp("S", "submit validation"), + ), } func (d *Model) handleKeyMsg(msg tea.KeyMsg) tea.Cmd { @@ -153,6 +158,14 @@ func (d *Model) handleKeyMsg(msg tea.KeyMsg) tea.Cmd { d.startConfirmDelete(selected.ID, fl.Index()) d.logAction("tui opened delete confirmation") } + case key.Matches(msg, d.keyMap.SubmitValidation): + if d.submitChan != nil { + select { + case d.submitChan <- struct{}{}: + d.logAction("tui submitted validation") + default: + } + } } return cmd diff --git a/pkg/tui/views/dashboard/model.go b/pkg/tui/views/dashboard/model.go index ae1fba5..de1ce4b 100644 --- a/pkg/tui/views/dashboard/model.go +++ b/pkg/tui/views/dashboard/model.go @@ -54,9 +54,10 @@ type Model struct { quitChan chan bool currentFeedback models.ValidationFeedback showComplete bool + submitChan chan<- struct{} } -func NewDashboard(app *app.App, feedbackChan chan models.ValidationFeedback, quitChan chan bool) *Model { +func NewDashboard(app *app.App, feedbackChan chan models.ValidationFeedback, quitChan chan bool, submitChan chan<- struct{}) *Model { m := &Model{ header: NewHeader("Project Manager Dashboard"), keyMap: defaultDashboardKeyMap, @@ -68,6 +69,7 @@ func NewDashboard(app *app.App, feedbackChan chan models.ValidationFeedback, qui focusedPaneClosed: 0, feedbackChan: feedbackChan, quitChan: quitChan, + submitChan: submitChan, } allIssues, _ := app.Issues.SearchIssues(context.Background(), "", models.IssueFilter{}) diff --git a/pkg/tui/views/views.go b/pkg/tui/views/views.go index 926a35c..1ba16e7 100644 --- a/pkg/tui/views/views.go +++ b/pkg/tui/views/views.go @@ -6,6 +6,6 @@ import ( "github.com/LazyBachelor/LazyPM/pkg/tui/views/dashboard" ) -func NewDashboardView(app *app.App, feedbackChan chan models.ValidationFeedback, quitChan chan bool) *dashboard.Model { - return dashboard.NewDashboard(app, feedbackChan, quitChan) +func NewDashboardView(app *app.App, feedbackChan chan models.ValidationFeedback, quitChan chan bool, submitChan chan<- struct{}) *dashboard.Model { + return dashboard.NewDashboard(app, feedbackChan, quitChan, submitChan) } From ae81e52f0dddbd8e63b8730d133d0def7f6527e3 Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Wed, 4 Mar 2026 22:45:31 +0100 Subject: [PATCH 44/51] change web and status command to use callback --- pkg/web/components/status.templ | 2 +- pkg/web/components/status_templ.go | 2 +- pkg/web/web.go | 9 +++++++++ 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/pkg/web/components/status.templ b/pkg/web/components/status.templ index 28b4ee1..63ea2de 100644 --- a/pkg/web/components/status.templ +++ b/pkg/web/components/status.templ @@ -1,7 +1,7 @@ package components templ Status() { -
+
} diff --git a/pkg/web/components/status_templ.go b/pkg/web/components/status_templ.go index 6db8cc5..0a2f7e1 100644 --- a/pkg/web/components/status_templ.go +++ b/pkg/web/components/status_templ.go @@ -29,7 +29,7 @@ func Status() templ.Component { templ_7745c5c3_Var1 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/pkg/web/web.go b/pkg/web/web.go index 4f23852..77b14c3 100644 --- a/pkg/web/web.go +++ b/pkg/web/web.go @@ -22,6 +22,7 @@ type ValidationFeedback = models.ValidationFeedback type Web struct { feedbackChan chan ValidationFeedback quitChan chan bool + submitChan chan<- struct{} } func New() *Web { @@ -72,6 +73,10 @@ func (w *Web) Run(ctx context.Context, config Config) error { }() } + if w.submitChan != nil { + handler.SetSubmitChan(w.submitChan) + } + select { case <-w.quitChan: fmt.Println("Task completed! Shutting down server...") @@ -89,3 +94,7 @@ func (w *Web) SetChannels(feedbackChan chan ValidationFeedback, quitChan chan bo w.feedbackChan = feedbackChan w.quitChan = quitChan } + +func (w *Web) SetSubmitChan(submitChan chan<- struct{}) { + w.submitChan = submitChan +} From 4a023caebb3952e8005885a48589a410ff696dff Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Thu, 5 Mar 2026 11:22:01 +0100 Subject: [PATCH 45/51] (hotfix) Align create issue task with new spec --- cmd/pm/tasks/createIssue.go | 45 +++++++++++++++++-------------------- 1 file changed, 21 insertions(+), 24 deletions(-) diff --git a/cmd/pm/tasks/createIssue.go b/cmd/pm/tasks/createIssue.go index 4dde38b..636f2ba 100644 --- a/cmd/pm/tasks/createIssue.go +++ b/cmd/pm/tasks/createIssue.go @@ -16,8 +16,7 @@ Your task: 1. Create a new issue with the title "My first Issue" 2. Add this detailed description "I need to do some coding" 3. Assign the issue to yourself as "Me" -4. Mark the issue as In Progress when you start working on it -5. Close the issue once you've completed the work +4. Mark the issue as In Progress when you are done. Make sure to fill out all the necessary details to help others understand the work item.` @@ -43,8 +42,7 @@ func (t *CreateIssueTask) Questions(interfaceType InterfaceType) Questions { return BaseQuestions(interfaceType) } -func (t *CreateIssueTask) QuestionnaireKeys(interfaceType InterfaceType) []string { - _ = interfaceType +func (t *CreateIssueTask) QuestionnaireKeys(_ InterfaceType) []string { return []string{"task_completed", "task_difficulty"} } @@ -61,8 +59,6 @@ func (t *CreateIssueTask) Setup(ctx context.Context) error { return t.app.Issues.CreateIssue(ctx, t.setupIssue, "") } -var isInProgress = false - func (t *CreateIssueTask) Validate(ctx context.Context) ValidationFeedback { expect := check.NewExpector() @@ -80,28 +76,29 @@ func (t *CreateIssueTask) Validate(ctx context.Context) ValidationFeedback { expect.Assert(len(issues) < 2, "Multiple issues were created instead of one. Delete the extra issues and try again.") - expect.NotEmptyString(issue.Title, "Issue title should not be empty") - expect.Assert(issue.Title == "My first Issue", - fmt.Sprintf("Issue title does not match the expected value 'My first Issue', but was '%s'", issue.Title)) - - expect.NotEmptyString(issue.Description, "Issue description should not be empty") - expect.Assert(issue.Description == "I need to do some coding", - fmt.Sprintf("Issue description does not match the expected value 'I need to do some coding', but was '%s'", issue.Description)) - - expect.Assert(issue.Assignee == "Me", - fmt.Sprintf("Issue should be assigned to 'Me', but was assigned to '%s'", issue.Assignee)) - - if issue.Status == models.StatusInProgress || isInProgress { - isInProgress = true + if issue.Title == "" { + expect.Fail("Issue title should not be empty, but it is empty") } else { - expect.Fail("Issue should be marked as in-progress when work starts") + expect.Assert(issue.Title == "My first Issue", + fmt.Sprintf("Issue title does not match the expected value 'My first Issue', but was '%s'", issue.Title)) } - if !isInProgress { - return expect.ValidationFeedback - } else if issue.Status != models.StatusClosed { - expect.Fail("Issue should be set to Closed once the work is completed") + if issue.Description == "" { + expect.Fail("Issue description should not be empty, but it is empty") + } else { + expect.Assert(issue.Description == "I need to do some coding", + fmt.Sprintf("Issue description does not match the expected value 'I need to do some coding', but was '%s'", issue.Description)) } + if issue.Assignee == "" { + expect.Fail("Issue should be assigned to 'Me', but it is not assigned to anyone") + } else { + expect.Assert(issue.Assignee == "Me", + fmt.Sprintf("Issue should be assigned to 'Me', but was assigned to '%s'", issue.Assignee)) + } + + expect.Assert(issue.Status == models.StatusInProgress, + fmt.Sprintf("Issue status should be 'In Progress', but was '%s'", issue.Status)) + return expect.Complete() } From 28add3f4ddcae41c849c5060b786b07cf0228c57 Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Thu, 5 Mar 2026 11:36:09 +0100 Subject: [PATCH 46/51] (hotfix) simplify expetion logic --- cmd/pm/tasks/createIssue.go | 25 ++++--------------------- internal/utils/check/expect.go | 24 +++++++++++++++++++----- 2 files changed, 23 insertions(+), 26 deletions(-) diff --git a/cmd/pm/tasks/createIssue.go b/cmd/pm/tasks/createIssue.go index 636f2ba..a600c47 100644 --- a/cmd/pm/tasks/createIssue.go +++ b/cmd/pm/tasks/createIssue.go @@ -74,28 +74,11 @@ func (t *CreateIssueTask) Validate(ctx context.Context) ValidationFeedback { issue := issues[0] - expect.Assert(len(issues) < 2, "Multiple issues were created instead of one. Delete the extra issues and try again.") + expect.Assert(len(issues) < 2, "Multiple issues were created instead of one") - if issue.Title == "" { - expect.Fail("Issue title should not be empty, but it is empty") - } else { - expect.Assert(issue.Title == "My first Issue", - fmt.Sprintf("Issue title does not match the expected value 'My first Issue', but was '%s'", issue.Title)) - } - - if issue.Description == "" { - expect.Fail("Issue description should not be empty, but it is empty") - } else { - expect.Assert(issue.Description == "I need to do some coding", - fmt.Sprintf("Issue description does not match the expected value 'I need to do some coding', but was '%s'", issue.Description)) - } - - if issue.Assignee == "" { - expect.Fail("Issue should be assigned to 'Me', but it is not assigned to anyone") - } else { - expect.Assert(issue.Assignee == "Me", - fmt.Sprintf("Issue should be assigned to 'Me', but was assigned to '%s'", issue.Assignee)) - } + expect.NotEmptyAndEqual(issue.Title, "My first Issue", "Issue title") + expect.NotEmptyAndEqual(issue.Description, "I need to do some coding", "Issue description") + expect.NotEmptyAndEqual(issue.Assignee, "Me", "Issue assignee") expect.Assert(issue.Status == models.StatusInProgress, fmt.Sprintf("Issue status should be 'In Progress', but was '%s'", issue.Status)) diff --git a/internal/utils/check/expect.go b/internal/utils/check/expect.go index 670fcdb..2b9b2f3 100644 --- a/internal/utils/check/expect.go +++ b/internal/utils/check/expect.go @@ -43,11 +43,6 @@ func (e *Expector) CompleteWithMessage(message string) ValidationFeedback { return e.ValidationFeedback } -func (e *Expector) Fail(message string) ValidationFeedback { - e.Checks = append(e.Checks, NewCheck(message, false)) - return e.ValidationFeedback -} - func (e *Expector) Errors() []error { var errors []error for _, check := range e.Checks { @@ -58,6 +53,25 @@ func (e *Expector) Errors() []error { return errors } +func (e *Expector) Pass(message string) *Expector { + e.Checks = append(e.Checks, NewCheck(message, true)) + return e +} + +func (e *Expector) Fail(message string) *Expector { + e.Checks = append(e.Checks, NewCheck(message, false)) + return e +} + +func (e *Expector) NotEmptyAndEqual(val, expected string, message string) *Expector { + if val == "" { + return e.Fail(fmt.Sprintf("%s is empty", message)) + } else if val != expected { + return e.Fail(fmt.Sprintf(`%s expected "%v", got "%v"`, message, expected, val)) + } + return e.Pass(message + " is correct") +} + func (e *Expector) Assert(condition bool, message string) *Expector { check := NewCheck(message, condition) e.Checks = append(e.Checks, check) From 2fc612c7c7ec6f72d3c175df066da9aa942618e9 Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Thu, 5 Mar 2026 12:03:13 +0100 Subject: [PATCH 47/51] update coding task to align with spec and improve expetion handling --- cmd/pm/tasks/codingTask.go | 60 ++++++++++------------------------ internal/utils/check/expect.go | 7 ++++ 2 files changed, 25 insertions(+), 42 deletions(-) diff --git a/cmd/pm/tasks/codingTask.go b/cmd/pm/tasks/codingTask.go index 5df0ec3..754f94c 100644 --- a/cmd/pm/tasks/codingTask.go +++ b/cmd/pm/tasks/codingTask.go @@ -2,7 +2,6 @@ package tasks import ( "context" - "fmt" "os" "strings" @@ -18,15 +17,14 @@ The MongoDB Driver dependency in the file is outdated and needs to be updated to This is a common task for developers, and it requires attention to detail and the ability to follow instructions carefully. Your task: -1. Assign this Issue to yourself as "Me" and mark it as "In Progress" -2. Create a New Issue and give it these details: +1. Create a New Issue and give it these details: - Title: "Upgrade MongoDB Driver Dependency" - Description: "We need to upgrade the MongoDB Driver dependency to the latest version." - Status: "In Progress" - Issue Type: "Chore" -3. A file will appear in the current directory named "code.txt". +2. A file will appear in the current directory named "code.txt". Open it and follow the instructions inside. And save the file after you are done. -4. When you are done, mark this and the issue you made as "Closed".` +3. When you are done, mark this and the issue you made as "Closed".` var textFileDescription = ` Please upgrade the MongoDB Driver dependency in the go.mod file to the latest version. @@ -122,7 +120,7 @@ func (t *CodingTask) Setup(ctx context.Context) error { return nil } -var codeTaskInProgress = false +var codingTaskInProgress = false func (t *CodingTask) Validate(ctx context.Context) ValidationFeedback { expect := check.NewExpector() @@ -132,47 +130,31 @@ func (t *CodingTask) Validate(ctx context.Context) ValidationFeedback { return expect.ValidationFeedback } - expect.Assert(t.setupIssue.Assignee == "Me", - "The original issue should be assigned to 'Me'.") - - expect.Assert(t.setupIssue.Status == models.StatusInProgress, - "The original issue should be marked as In Progress before starting work.") - - expect.Assert(len(issues) > 0, - "No new issues created. Please create an issue with the specified details.") - if len(issues) == 0 { expect.Fail("No new issues created") return expect.ValidationFeedback + } else { + expect.Pass("An issue was created") } issue := issues[0] - expect.Assert(len(issues) < 2, - "Multiple issues were created instead of one. Delete the extra issues and try again.") + expect.Assert(len(issues) < 2, "Multiple issues were created instead of one") - expect.NotEmptyString(issue.Title, - "Issue title should not be empty") + expect.NotEmptyAndEqual(issue.Title, "Upgrade MongoDB Driver Dependency", "Issue title") - expect.Assert(issue.Title == "Upgrade MongoDB Driver Dependency", - fmt.Sprintf("Issue title does not match the expected value 'Upgrade MongoDB Driver Dependency', but was '%s'", issue.Title)) + expect.NotEmptyAndEqual(issue.Description, + "We need to upgrade the MongoDB Driver dependency to the latest version.", "Issue description") - expect.NotEmptyString(issue.Description, - "Issue description should not be empty") + expect.NotEmptyAndEqual(issue.Assignee, "Me", "Issue Assignee") - expect.Assert(issue.Description == "We need to upgrade the MongoDB Driver dependency to the latest version.", - fmt.Sprintf("Issue description does not match 'We need to upgrade the MongoDB Driver dependency to the latest version.', but was '%s'", issue.Description)) + expect.Equal(issue.IssueType, models.TypeChore, "Issue type") - expect.Assert(issue.IssueType == models.TypeChore, - fmt.Sprintf("Issue type should be 'Chore', but was '%s'", issue.IssueType)) - - expect.Assert(issue.Assignee == "Me", - fmt.Sprintf("Issue should be assigned to 'Me', but was assigned to '%s'", issue.Assignee)) - - if issue.Status == models.StatusInProgress || codeTaskInProgress { - codeTaskInProgress = true + if issue.Status == models.StatusInProgress || codingTaskInProgress { + codingTaskInProgress = true } else { - expect.Fail("Issue should be marked as In Progress when work starts") + expect.Fail("The issue should be marked as In Progress while working on the task.") + return expect.ValidationFeedback } if _, err := os.Stat("./code.txt"); os.IsNotExist(err) { @@ -195,14 +177,8 @@ func (t *CodingTask) Validate(ctx context.Context) ValidationFeedback { expect.Assert(strings.Contains(code, "go.mongodb.org/mongo-driver v1.17.9"), "The MongoDB Driver dependency should be updated to version v1.17.9 in the file.") - if !codeTaskInProgress { - return expect.ValidationFeedback - } else if issue.Status != models.StatusClosed { - expect.Fail("Issue should be set to Closed once the work is completed") - } else { - expect.Assert(t.setupIssue.Status == models.StatusClosed, - "The original setup issue should be set to Closed once the work is completed") - } + expect.Assert(codingTaskInProgress && issue.Status == models.StatusClosed, + "The issue should be marked as Closed after completing the task.") return expect.Complete() } diff --git a/internal/utils/check/expect.go b/internal/utils/check/expect.go index 2b9b2f3..8850d83 100644 --- a/internal/utils/check/expect.go +++ b/internal/utils/check/expect.go @@ -72,6 +72,13 @@ func (e *Expector) NotEmptyAndEqual(val, expected string, message string) *Expec return e.Pass(message + " is correct") } +func (e *Expector) Equal(val, expected any, message string) *Expector { + if val != expected { + return e.Fail(fmt.Sprintf(`%s expected "%v", got "%v"`, message, expected, val)) + } + return e.Pass(message + " is correct") +} + func (e *Expector) Assert(condition bool, message string) *Expector { check := NewCheck(message, condition) e.Checks = append(e.Checks, check) From e94701001cf7a513919af164eaaabaef9bb93446 Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Thu, 5 Mar 2026 12:12:47 +0100 Subject: [PATCH 48/51] remove notebook data --- notebooks/task-data-analysis.ipynb | 2053 +--------------------------- 1 file changed, 25 insertions(+), 2028 deletions(-) diff --git a/notebooks/task-data-analysis.ipynb b/notebooks/task-data-analysis.ipynb index 4765eac..de212e6 100644 --- a/notebooks/task-data-analysis.ipynb +++ b/notebooks/task-data-analysis.ipynb @@ -11,7 +11,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "id": "8626d9d2", "metadata": {}, "outputs": [], @@ -28,30 +28,10 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "id": "7bc3e98d", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Working directory: /home/robino/Projects/Go/LazyPM/notebooks\n", - "Using stats directory: /home/robino/Projects/Go/LazyPM/.pm\n", - "Found 1 stats file(s).\n" - ] - }, - { - "data": { - "text/plain": [ - "[PosixPath('/home/robino/Projects/Go/LazyPM/.pm/coding-task-stats.json')]" - ] - }, - "execution_count": 3, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "candidate_dirs = [\n", " Path.cwd(),\n", @@ -77,18 +57,10 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "id": "c453e1ad", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Loaded 1 task dataset(s).\n" - ] - } - ], + "outputs": [], "source": [ "datasets = []\n", "for file in stat_files:\n", @@ -102,21 +74,10 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "id": "cbb0dd6b", "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "((1, 19), (19, 18), (2263, 12))" - ] - }, - "execution_count": 5, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "summary_rows = []\n", "run_rows = []\n", @@ -173,82 +134,10 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": null, "id": "92aea6d2", "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
task_nametotal_runscompleted_runsincomplete_runsaverage_duration_msvalidation_attemptsvalidation_successesvalidation_failurestotal_user_actionscompletion_rate_pctavg_duration_s
0Coding Task19118710813431342595.371.08
\n", - "
" - ], - "text/plain": [ - " task_name total_runs completed_runs incomplete_runs \\\n", - "0 Coding Task 19 1 18 \n", - "\n", - " average_duration_ms validation_attempts validation_successes \\\n", - "0 71081 343 1 \n", - "\n", - " validation_failures total_user_actions completion_rate_pct \\\n", - "0 342 59 5.3 \n", - "\n", - " avg_duration_s \n", - "0 71.08 " - ] - }, - "execution_count": 6, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "summary_cols = [\n", " \"task_name\",\n", @@ -274,21 +163,10 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": null, "id": "6bd0a081", "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAxYAAAGGCAYAAADmRxfNAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjgsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvwVt1zgAAAAlwSFlzAAAPYQAAD2EBqD+naQAANpNJREFUeJzt3X18z3X////7e3a+YdjYZrNh5GxMVAclhiEHHZ3I+UFJpajUgUJHpk+EUihyJDk7QuNAitAJk/McQ87K6Q4NY7ONjLGT1+8P371/3m1jPKe3k9v1culS7+fr9Xq+Hq/XU/W+v1/P1+tlsyzLEgAAAAAYcHF2AQAAAABufQQLAAAAAMYIFgAAAACMESwAAAAAGCNYAAAAADBGsAAAAABgjGABAAAAwBjBAgAAAIAxggUAAAAAYwQLALhDxMbGymazac2aNTd0Py1atJDNZruh+7iT/FnjdrMJDw9XeHi4s8sAcA0IFgDw//zyyy968cUXVa9ePZUtW1bu7u4KDg7WX//6V02fPl0XLlxwdok3hSeffFI2m02JiYnOLsVpEhMTZbPZ9OSTTzq7lBvqTg01AK6Pq7MLAICbwVtvvaWRI0cqLy9PTZo0Ue/eveXr66sTJ05ozZo16tu3rz7++GNt3brV2aXe9GbPnq1z5845uwwAwJ+MYAHgjjd69GiNGDFCoaGhWrBgge67774C63z99dcaP368E6q79VSpUsXZJQAAnICpUADuaImJiYqNjZWbm5uWL19eaKiQpA4dOmjFihUF2uPi4vTggw+qbNmy8vLyUmRkpN55551Cp03lzxk/e/asXnnlFYWGhsrLy0tRUVFasmSJJCknJ0ejRo1SjRo15OnpqerVq+ujjz4q0NeaNWtks9kUGxurjRs3qnXr1ipbtqxKly6ttm3bXvOVlV9++UVPPvmkQkND5e7urkqVKql79+769ddfHdaz2WyaNWuWJKlq1aqy2Wyy2WwOc+GLusciLy9PU6dO1T333CNfX1/5+Pjonnvu0ccff6y8vLwC69tsNrVo0UKpqal69tlnFRQUJA8PD9WtW1czZsy4puPLP/dnzpzRq6++qvDwcLm5uSk2NlaSdOzYMb311lu6//77FRgYaJ8G1717d+3Zs8ehr9jYWFWtWlWSNGvWLPs5sNlsmjlzpsO6K1euVPv27eXv7y8PDw9Vr15dgwcPVkZGxjXVn2/WrFlq2LChvLy8VLFiRfXp00fJyckO6zRp0kQuLi5FTlUbP368bDab3nvvvSvuKzw8XCNHjpQkRUdHOxxnvn379un1119X48aNFRAQIA8PD4WFhenZZ59VUlJSgT4ty9KsWbPUtGlTBQQEyNPTU6GhoWrbtq2++OKLYp2DuXPnysPDQ7Vr176jp+MBNyOuWAC4o82YMUPZ2dnq2rWr6tWrd8V1PTw8HD4PGzZM77zzjvz9/dW9e3f5+vrqm2++0bBhw7Ry5UqtWrVK7u7uDttkZ2crJiZGaWlp+tvf/qaLFy9q3rx5evzxx7Vq1SpNmTJFmzdv1kMPPSQPDw8tWLBAL774ogICAtSlS5cCNW3evFnvvPOOWrdurf79++vAgQNatGiR1q5dq1WrVqlZs2ZXPQcrVqzQY489puzsbHXs2FERERFKSkrSokWLtGzZMq1evVp33323JGnEiBFasmSJduzYoZdffll+fn6SZP/7lfz973/X3LlzFRoaqr59+8pms2nx4sV64YUXtG7dOn3++ecFtsnIyND9998vd3d3derUSRcuXNCCBQvUp08fubi4qHfv3lfdb76LFy+qZcuWSktLU5s2bVSmTBl7QFi7dq3GjBmj6OhoPf744/L19dX+/fu1cOFCLV26VOvXr1eDBg0kXQpOGRkZmjhxoho0aKBHHnnEvo+oqCj7P48cOVKxsbEqX768OnTooIoVK+rnn3/We++9p+XLl2vjxo0qU6ZMsev/4IMPtGrVKnXp0kXt2rXTunXrNGPGDK1Zs0abN29WQECAJOn555/Xpk2bNG3aNI0aNapAP5988ok8PDyuen/IwIEDtWTJEsXHx6t3796F3ki9aNEiTZ06VdHR0WratKnc3d21e/duffrpp/rqq6+0detWVa5c2b7+8OHD9c4776hq1arq3LmzypYtq+PHj+unn37SggULCv0zfrlx48bp9ddfV9OmTbV06VKVL1/+6icOwJ/HAoA7WMuWLS1J1rRp065puw0bNliSrNDQUOv48eP29uzsbKtDhw6WJGvUqFEO24SFhVmSrA4dOlhZWVn29rVr11qSrHLlylmNGze20tPT7csOHjxoubm5WVFRUQ59rV692pJkSbI+/PBDh2VLliyxJFkRERFWbm6uvX3EiBGWJGv16tX2trS0NMvPz8+qUKGC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Xbx4UX/961+LfP/DihUr9NBDD9k/9+nTR5L09ttvKyUlxd6em5urQYMGKS8v75qugFyv/fv3a8qUKQ5tX375peLj4xUREXHVx80+9dRT8vPz08iRI7Vly5YCy/Py8gqEp/ypO0eOHCl2nfnna+jQoQ5v5T537pxef/11SfpTzldhKlasKG9vb/33v/91mPKVnZ2tl19+WampqQW2KVeunGw2W5Hn4JVXXpEkPfPMMzp27FiB5ZmZmdq0adM11TlnzpwC96HExsbq9OnT6tatW4HHIbu4uOjZZ5/VyZMn7ee/X79+17TPK411/uNn161b5xA8zp49q2eeeabAPSoXLlzQ+vXrC/STnZ1tv7Lj7e1daB3NmjXTt99+K5vNpjZt2hQasgA4H1OhANzxhg0bZp8Pf88996hp06Zq3LixfH19deLECa1du1b79+93mILRtGlTDRkyROPGjVO9evXUqVMn+fj46JtvvtGuXbv0wAMPaPDgwTe89nbt2ukf//iHvvnmGzVo0MD+HgtPT0999tlnhd7YfbkKFSpo4cKFevTRR/WXv/xFrVq1Ut26dWWz2fTbb79p48aNOnXqlLKysuzbtGrVSu+++66eeeYZPf744ypdurT8/Pw0YMCAIvfTvXt3ffnll4qLi1PdunX1yCOP2O9fOHz4sLp06aIePXqU2Hm5Fi4uLnrppZc0ZswYRUZG2t8vsnr1aqWlpSk6OlqrV6922MbX11f33XeffvzxR/Xo0UM1a9ZUqVKl9PDDD6t+/fpq1aqVxowZo6FDh6pGjRpq3769qlatqrNnz+p///uf4uPj9cADDxT60sWiPPTQQ7r//vvVuXNnBQUFad26dVq3bp3Cw8M1ZsyYQrfp27ev3nrrLR09elSRkZFq0qTJNZ2b6Ohoubi4aOjQodq1a5f9ascbb7yhwMBAde3aVfPnz1dUVJTatGmj06dP69tvv5Wnp6eioqK0fft2e1/nz5/XAw88oIiICDVq1EhhYWHKysrSt99+q7179+rhhx8ucNXlcvfdd59++OEHxcTEqH379lqyZIliYmKu6XgA3GDOfiwVANws9uzZYw0YMMCqW7euVbp0acvNzc0KDAy02rVrZ3366aeFPiJz3rx51v3332/5+vpaHh4eVp06day333670OfsX/5c/j8q7BGt+Qp7vGv+42ZHjBhhbdiwwWrVqpVVunRpy9fX14qJibG2bNlSoJ8rPTr08OHDVv/+/a2IiAjLw8PDKl26tHXXXXdZPXv2tBYvXlxg/fHjx1u1atWy3N3dLUkOx1XUseTm5lqTJ0+2GjVqZHl5eVleXl7W3XffbX300UeFPkJUktW8efNin5MrudK5t6xL7x8ZP368Vbt2bcvT09OqVKmS1bNnTysxMbHIfe3fv9/q0KGDVb58ectms1mSrBkzZjis8+OPP1pPPPGEFRQUZLm5uVn+/v5WgwYNrFdeecX66aefilX75eM2Y8YMq0GDBpanp6fl7+9vPfnkk9axY8euuH3+o2+LeqTw1cyZM8e+T/2/d4vky8zMtIYNG2ZVr17d8vDwsEJCQqwXXnjBSk1NLfDn4OLFi9bYsWOtdu3aWaGhoZaHh4fl7+9v3XfffdbHH39sXbhwwWG/RY3Zzp07rUqVKlkeHh7W119/fV3HBODGsFmWZTkhzwAADKxZs0bR0dEaMWJEoY+JBaRLU9kiIiJ04sQJHT9+/IbfIA/gzsY9FgAA3KYWLlyow4cPq1evXoQKADcc91gAAHCbGTNmjNLS0vTJJ5/Ix8dHQ4cOdXZJAO4ABAsAAG4zQ4cOlZubm+rUqaN3331XVapUcXZJAO4A3GMBAAAAwBj3WAAAAAAwRrAAAAAAYIxgAQAAAMAYwQIAAACAMZ4KdZNLT09XTk6Os8vADRQQEKCUlBRnl4EbiDG+MzDOdwbG+fbHGDtydXVVuXLlirfuDa4FhnJycpSdne3sMnCD2Gw2SZfGmQe03Z4Y4zsD43xnYJxvf4yxGaZCAQAAADBGsAAAAABgjGABAAAAwBjBAgAAAIAxggUAAAAAYwQLAAAAAMYIFgAAAACMESwAAAAAGCNYAAAAADBGsAAAAABgjGABAAAAwBjBAgAAAIAxggUAAAAAYwQLAAAAAMYIFgAAAACMESwAAAAAGCNYAAAAADBGsAAAAABgjGABAAAAwBjBAgAAAIAxggUAAAAAYwQLAAAAAMYIFgAAAACMESwAAAAAGCNYAAAAADBGsAAAAABgjGABAAAAwBjBAgAAAIAxggUAAAAAYwQLAAAAAMYIFgAAAACMESwAAAAAGCNYAAAAADBGsAAAAABgjGABAAAAwBjBAgAAAIAxggUAAAAAYwQLAAAAAMYIFgAAAACMESwAAAAAGCNYAAAAADBGsAAAAABgjGABAAAAwBjBAgAAAIAxggUAAAAAYwQLAAAAAMYIFgAAAACMESwAAAAAGCNYAAAAADBGsAAAAABgjGABAAAAwBjBAgAAAIAxggUAAAAAYwQLAAAAAMYIFgAAAACMESwAAAAAGCNYAAAAADBGsAAAAABgjGABAAAAwBjBAgAAAIAxggUAAAAAYwQLAAAAAMYIFgAAAACMESwAAAAAGCNYAAAAADBGsAAAAABgjGABAAAAwBjBAgAAAIAxggUAAAAAYwQLAAAAAMYIFgAAAACMESwAAAAAGLNZlmU5uwgULfmlHso++KuzywAAAICTlJq21Gn7dnNzU0BAQLHW5YoFAAAAAGMECwAAAADGCBYAAAAAjBEsAAAAABgjWAAAAAAwRrAAAAAAYIxgAQAAAMAYwQIAAACAMYIFAAAAAGMECwAAAADGCBYAAAAAjBEsAAAAABgjWAAAAAAwRrAAAAAAYIxgAQAAAMAYwQIAAACAMYIFAAAAAGMECwAAAADGCBYAAAAAjBEsAAAAABgjWAAAAAAwRrAAAAAAYIxgAQAAAMAYwQIAAACAMYIFAAAAAGMECwAAAADGCBYAAAAAjBEsAAAAABgjWAAAAAAwRrAAAAAAYIxgAQAAAMAYwQIAAACAMYIFAAAAAGMECwAAAADGCBYAAAAAjBEsAAAAABgjWAAAAAAwRrAAAAAAYIxgAQAAAMAYwQIAAACAMYIFAAAAAGMECwAAAADGCBYAAAAAjBEsAAAAABgjWAAAAAAwRrAAAAAAYIxgAQAAAMAYwQIAAACAMYIFAAAAAGMECwAAAADGCBYAAAAAjBEsAAAAABgjWAAAAAAwRrAAAAAAYIxgAQAAAMAYwQIAAACAMYIFAAAAAGMECwAAAADGCBYAAAAAjBEsAAAAABgjWAAAAAAwRrAAAAAAYIxgAQAAAMAYwQIAAACAMYIFAAAAAGMECwAAAADGCBYAAAAAjBEsAAAAABgjWAAAAAAwdtMEi7i4OA0ePNj+efLkyRo3bpwTKzITGxurmTNnOrsMAAAA4E/heq0bZGRkaNGiRUpISFBaWprKli2rsLAw/fWvf1VkZGSJFfbUU0/JsqwS668wnTt3vuLyTp06XXUdAAAAANcYLE6ePKl//vOf8vHxUc+ePVWlShXl5uZqx44dmj59uiZMmFBihXl7e5dYX0X55JNP7P+8YcMGffHFF5o4caK9zdPT84bXAAAAANwOrilYTJ8+XTabTaNHj3b40h0aGqro6Gj759TUVH322WfauXOnXFxc1KBBA/Xp00d+fn72dZYsWaJly5bpwoULatKkicqUKeOwr8mTJyszM1NDhgyRdGlqUZUqVeTu7q7vv/9erq6uiomJcbiicPToUU2dOlWHDh1SxYoV9dRTT+ntt9/WoEGDdO+99xY4nsvr8fb2ls1ms7clJyfrk08+0f79+5WVlaWQkBB169ZN9evXt2+zcuVKLVu2TKdOnZK3t7dq1aqlf/zjH4Weu4SEBE2cOFF9+/ZVs2bNrn6yAQAAgFtIsYPF2bNntX37dnXt2rXQX/J9fHwkSXl5eRo3bpw8PT01cuRI5ebm2q9mxMbGSrp0dWDBggV6+umnVatWLa1du1bffPONKlaseMUa4uPj1aFDB40ePVr79u3TlClTVKtWLdWvX195eXl699135e/vr1GjRikrK0uzZ8++hlPhKCsrSw0bNlTXrl3l5uam+Ph4jR07VhMnTpS/v78OHjyoGTNmaMCAAbrrrrt09uxZ7d27t9C+1q1bp2nTpumll15So0aNCl0nOztb2dnZ9s82m01eXl7XXT8AAABuDzabzdklFEuxg0VycrIsy1LlypWvuN6uXbt05MgRffTRR/L395ckDRgwQK+++qoOHDigiIgILV++XNHR0WrZsqUkqWvXrtq5c6cuXrx4xb7DwsL0xBNPSJKCgoK0YsUK7dy5U/Xr19fPP/+sEydOKDY21n7VoWvXrnr77beLe4gOwsPDFR4ebv/ctWtX/fTTT9q6davatWun1NRUeXh4qFGjRvLy8lJAQICqVq1aoJ8VK1Zo/vz5eu2111SnTp0i97d48WItXLjQ/rlq1aoaO3bsddUOAACA20dQUJCzSyiWYgeL4t5InZSUpAoVKthDhSSFhITIx8dHR48eVUREhI4ePaqYmBiH7WrUqKHdu3dfse8qVao4fC5XrpxOnz4tSTp27JgqVKjgML0pIiKiWDUXJisrS3Fxcdq2bZvS09OVm5urixcvKjU1VZJUv359BQQEaMCAAYqKilJUVJTuvfdeeXh42PvYtGmTTp8+rf/7v/+7ai2PPvqoOnToYP98qyRTAAAA3FjHjx932r5dXV0VEBBQvHWL22lQUJBsNpuOHj163YWZcnUtWO6NenLU7NmztXPnTv39739XYGCg3N3dNX78eOXk5EiSvLy8NHbsWO3evVs///yz4uLitGDBAr3zzjv2aWHh4eE6fPiwVq9ererVq18xLLi5ucnNze2GHAsAAABuXTf6SaklpdjvsfD19VWDBg20cuVKZWVlFViemZkp6dLViVOnTtl/2ZcuXcXIzMxUSEiIJKly5crav3+/w/Z//HytgoODderUKWVkZNjbDh48eN39/frrr2revLnuvfdeValSRX5+fkpJSXFYp1SpUqpfv7569uypd999VykpKdq1a5d9eWBgoEaMGKGtW7fqs88+u+5aAAAAgJvdNb0g7+mnn1ZeXp6GDRumTZs26fjx40pKStLy5cv1xhtvSJIiIyNVpUoVffjhhzp06JAOHDigjz76SHXq1FH16tUlSe3bt9fq1au1evVqHTt2THFxcUpKSjI6kPr166tSpUqaPHmy/ve//+mXX37R/PnzJV3ftKKgoCBt2bJFiYmJSkxM1MSJEx3S4n//+18tX75ciYmJSklJ0dq1a5WXl6fg4GCHfoKDgzVixAht3ryZF+YBAADgtnVNj5utVKmSxo4dq0WLFmnOnDlKT09XmTJlVK1aNfXt21fSpS/xQ4YM0WeffaYRI0Y4PG42X9OmTZWcnKx///vfys7O1n333aeYmBjt2LHjug/ExcVFgwcP1tSpUzV06FBVqlRJPXv21NixY69rilGvXr308ccf64033lDp0qX1t7/9TefPn7cv9/Hx0ZYtW7RgwQJlZ2crKChIL7/8skJDQwv0FRwcrDfffFMjR46Ui4uLevXqdd3HCQAAANyMbNatMmnrOvzyyy968803NWnSJAUGBjq7nOuS/FIPZR/81dllAAAAwElKTVvqtH27ubmV/M3bt4ItW7bI09NTgYGBSk5O1syZM3XXXXfdsqECAAAAuFXcVsHi/Pnz+vzzz5WamqrSpUsrMjKSaUcAAADAn+C2ChbNmzdX8+bNnV0GAAAAcMe5pqdCAQAAAEBhCBYAAAAAjBEsAAAAABgjWAAAAAAwRrAAAAAAYIxgAQAAAMAYwQIAAACAMYIFAAAAAGMECwAAAADGCBYAAAAAjBEsAAAAABgjWAAAAAAwRrAAAAAAYIxgAQAAAMAYwQIAAACAMYIFAAAAAGMECwAAAADGCBYAAAAAjBEsAAAAABgjWAAAAAAwRrAAAAAAYIxgAQAAAMAYwQIAAACAMYIFAAAAAGMECwAAAADGCBYAAAAAjBEsAAAAABgjWAAAAAAwRrAAAAAAYIxgAQAAAMAYwQIAAACAMYIFAAAAAGMECwAAAADGCBYAAAAAjBEsAAAAABgjWAAAAAAwRrAAAAAAYIxgAQAAAMAYwQIAAACAMYIFAAAAAGMECwAAAADGCBYAAAAAjBEsAAAAABgjWAAAAAAwRrAAAAAAYIxgAQAAAMAYwQIAAACAMYIFAAAAAGMECwAAAADGCBYAAAAAjBEsAAAAABgjWAAAAAAwRrAAAAAAYIxgAQAAAMAYwQIAAACAMYIFAAAAAGMECwAAAADGCBYAAAAAjBEsAAAAABgjWAAAAAAwRrAAAAAAYIxgAQAAAMAYwQIAAACAMYIFAAAAAGMECwAAAADGbJZlWc4uAkVLSUlRdna2s8vADWKz2RQUFKTjx4+LfxVvT4zxnYFxvjMwzrc/xrggNzc3BQQEFGtdrlgAAAAAMEawAAAAAGCMYAEAAADAGMECAAAAgDGCBQAAAABjBAsAAAAAxggWAAAAAIwRLAAAAAAYI1gAAAAAMEawAAAAAGCMYAEAAADAGMECAAAAgDGCBQAAAABjBAsAAAAAxggWAAAAAIwRLAAAAAAYI1gAAAAAMEawAAAAAGCMYAEAAADAGMECAAAAgDGCBQAAAABjBAsAAAAAxggWAAAAAIwRLAAAAAAYI1gAAAAAMEawAAAAAGCMYAEAAADAGMECAAAAgDGCBQAAAABjBAsAAAAAxggWAAAAAIwRLAAAAAAYI1gAAAAAMEawAAAAAGCMYAEAAADAGMECAAAAgDGCBQAAAABjBAsAAAAAxggWAAAAAIwRLAAAAAAYI1gAAAAAMEawAAAAAGCMYAEAAADAGMECAAAAgDGCBQAAAABjBAsAAAAAxggWAAAAAIwRLAAAAAAYI1gAAAAAMEawAAAAAGCMYAEAAADAGMECAAAAgDGCBQAAAABjBAsAAAAAxggWAAAAAIwRLAAAAAAYI1gAAAAAMEawAAAAAGCMYAEAAADAGMECAAAAgDGCBQAAAABjBAsAAAAAxggWAAAAAIwRLAAAAAAYI1gAAAAAMEawAAAAAGCMYAEAAADAGMECAAAAgDGCBQAAAABjBAsAAAAAxggWAAAAAIwRLAAAAAAYI1gAAAAAMObq7AJwZa6uDNGdgHG+/THGdwbG+c7AON/+GOP/37WcC5tlWdYNrAXXKTs7W25ubs4uAwAAACgWpkLdpLKzszVx4kSdP3/e2aXgBjp//rxee+01xvk2xhjfGRjnOwPjfPtjjM0QLG5i69evFxeUbm+WZenw4cOM822MMb4zMM53Bsb59scYmyFYAAAAADBGsAAAAABgjGBxk3Jzc1OnTp24gfs2xzjf/hjjOwPjfGdgnG9/jLEZngoFAAAAwBhXLAAAAAAYI1gAAAAAMEawAAAAAGCM95XfhFasWKGvvvpKGRkZCgsLU58+fRQREeHsslCC9uzZo6VLl+rw4cNKT0/XoEGDdO+99zq7LJSgxYsXa8uWLTp69Kjc3d1Vs2ZN9ezZU8HBwc4uDSVo1apVWrVqlVJSUiRJISEh6tSpkxo2bOjkynCjLFmyRHPnzlX79u315JNPOrsclJC4uDgtXLjQoS04OFgTJkxwTkG3KILFTWbDhg2aPXu2nnnmGdWoUUPLli3TqFGjNGHCBJUtW9bZ5aGEXLhwQeHh4WrZsqXee+89Z5eDG2DPnj1q27atqlevrtzcXM2bN09vv/223n//fXl6ejq7PJSQ8uXLq3v37goKCpJlWYqPj9e4ceM0btw4hYaGOrs8lLADBw7o22+/VVhYmLNLwQ0QGhqqf/7zn/bPLi5M7LlWnLGbzNdff61WrVopOjpaISEheuaZZ+Tu7q7Vq1c7uzSUoIYNG6pr165cpbiNDR8+XC1atFBoaKjCw8PVv39/paam6tChQ84uDSWocePGuvvuuxUUFKTg4GB169ZNnp6e2r9/v7NLQwnLysrShx9+qOeee04+Pj7OLgc3gIuLi/z8/Ox/lSlTxtkl3XIIFjeRnJwcHTp0SJGRkfY2FxcXRUZGat++fU6sDICpc+fOSZJ8fX2dXAlulLy8PK1fv14XLlxQzZo1nV0OStinn36qhg0bqn79+s4uBTdIcnKynnvuOQ0YMECTJk1Samqqs0u65TAV6iZy5swZ5eXlyc/Pz6Hdz89Px44dc05RAIzl5eVp5syZuuuuu1SlShVnl4MSduTIEQ0fPlzZ2dny9PTUoEGDFBIS4uyyUILWr1+vw4cP65133nF2KbhBatSooRdeeEHBwcFKT0/XwoUL9eabb2r8+PHy8vJydnm3DK5YAMANNn36dP32228aOHCgs0vBDRAcHKx3331Xo0ePVps2bTR58mQlJSU5uyyUkNTUVM2cOVMvvfSS3N3dnV0ObpCGDRuqSZMmCgsLU1RUlIYOHarMzExt3LjR2aXdUrhicRMpU6aMXFxclJGR4dCekZFR4CoGgFvD9OnTlZCQoJEjR6pChQrOLgc3gKurqwIDAyVJ1apV08GDB7V8+XI9++yzTq4MJeHQoUM6ffq0XnvtNXtbXl6e9u7dqxUrVmju3Lnc5Hsb8vHxUXBwsJKTk51dyi2FYHETcXV1VbVq1bRr1y77Tb15eXnatWuX2rVr5+TqAFwLy7L02WefacuWLYqNjVXFihWdXRL+JHl5ecrOznZ2GSghkZGRBZ7e9/HHHys4OFh/+9vfCBW3qaysLCUnJ6tZs2bOLuWWQrC4yXTo0EGTJ09WtWrVFBERoeXLl+vChQtq0aKFs0tDCcr/D1a+kydPKjExUb6+vvL393diZSgp06dP17p16zRkyBB5eXnZr0R6e3szneI2MnfuXEVFRcnf319ZWVlat26d9uzZo+HDhzu7NJQQLy+vAvdGeXh4qHTp0twzdRuZPXu2GjduLH9/f6WnpysuLk4uLi564IEHnF3aLYVgcZNp2rSpzpw5o7i4OGVkZCg8PFzDhg1jKtRt5uDBgxo5cqT98+zZsyVJzZs3V//+/Z1VFkrQqlWrJEmxsbEO7S+88AI/FNxGTp8+rcmTJys9PV3e3t4KCwvT8OHDeXIQcItJS0vTxIkT9fvvv6tMmTKqVauWRo0axSNnr5HNsizL2UUAAAAAuLUxMRAAAACAMYIFAAAAAGMECwAAAADGCBYAAAAAjBEsAAAAABgjWAAAAAAwRrAAAAAAYIxgAQAAAMAYwQIAcEN07txZcXFxJdrn5MmTeTt9EQ4cOKBu3bopJSWlRPpLSkpS165ddeTIkRLpD8Dtz9XZBQAAipacnKylS5fq559/Vnp6ulxdXVWlShU1adJErVu3lru7u7NLLHFpaWn67rvvdO+99yo8PNzZ5ZSYG31c8+bN0/3336+AgAB725YtWzR37lylp6erdu3aevbZZ1W+fHmH7caOHSs/Pz8999xzDu0hISG6++67FRcXp0GDBpV4vQBuP1yxAICbVEJCggYNGqSNGzeqUaNGeuqpp9StWzdVqFBBc+bM0YwZM5xd4g2Rnp6uhQsXKjExscCy5557ThMmTPjTayoJVzouU4mJidq5c6fatGljbztx4oQmTJigiIgI9ejRQ8ePH9fHH3/ssN327du1d+9edevWrdB+Y2JitGXLFiUnJ5d4zQBuP1yxAICb0MmTJzVhwgQFBATozTffVLly5ezL2rVrp+TkZCUkJDixQudwdb15/rd18eJFubq6ysXF+b/RrV69Wv7+/qpRo4a9bceOHapQoYL69+8vm82mypUr66233tLFixfl7u6u3NxczZo1S506dVKZMmUK7TcyMlI+Pj6Kj49Xly5d/qzDAXCLunn+Cw0AsPvyyy+VlZWlfv36OYSKfIGBgWrfvr39c25urhYvXqz4+HidOnVK5cqV0/33368nnnhCbm5u9vX69++v0NBQdezYUXPmzNFvv/2mwMBA9enTR3Xr1tXmzZsVFxen5ORkhYSEqF+/fqpatap9+8mTJ2vTpk1677339Omnn+qXX36Rt7e3YmJi9Pjjj8tms13xuNLS0jR//nxt27ZNmZmZCgwMVIcOHdSyZUtJ0u7duzVy5EhJ0pQpUzRlyhRJ0gsvvKAWLVpo8uTJ2rNnjyZPnmzvMysrS3Fxcdq4caNOnz6tgIAAtWrVSh07dnSop3Pnzmrbtq0iIyP1xRdf6Pjx4woMDFSvXr0UFRV1xbrz63r55Zf122+/afXq1crIyNBnn30my7K0aNEi7dixQydPnpSLi4vuuusude/e3T7l6WrHJUn79+9XXFyc9u3bp9zcXFWvXl3dunVTrVq1rlibJP3000+qV6+ew/FevHhR3t7e9jZfX19ZlmUPFitWrFBeXp7atWtXZL+urq6qW7eutm7dSrAAcFXO/5kFAFDAf//7X1WqVEl33XVXsdafOnWq4uLiVLVqVfXu3Vu1a9fWkiVLCp02lJycrEmTJqlRo0bq3r27MjMzNXbsWP3444+aNWuWmjVrpieeeEInTpzQBx98oLy8PIft8/LyNHr0aJUtW1Y9evRQ1apVFRcXd9UbtTMyMjR8+HDt3LlTbdu21ZNPPqnAwEBNnTpVy5YtkyRVrlxZnTt3liS1bt1aAwYM0IABA1S7du1C+7QsS+PGjdOyZcvUoEED9erVS8HBwfr3v/+tWbNmFVj/l19+0fTp09W0aVP17NlT2dnZGj9+vH7//ffinGb95z//UUJCgjp27Khu3brJ1dVVJ06c0E8//aRGjRqpd+/e6tixo44cOaLY2FilpaUV67h27dqlESNG6Pz583riiSfUrVs3nTt3Tm+99ZYOHDhwxZrS0tKUmprqEAAlKSIiQomJiVq3bp1OnjypRYsWKTAwUL6+vjpz5owWLlyo3r17X/UqULVq1XTkyBGdO3euWOcIwJ2LKxYAcJM5d+6c0tLS1Lhx42Ktn5iYqPj4eLVs2VL9+vWTJLVt21Zly5bVV199pV27dqlevXr29Y8dO6a3335bNWvWlHTpJt1Ro0bpX//6lyZMmCB/f39Jl37h/uSTT7R3717VrVvXvn12drYaNGigPn362Pc1duxYffnll3rooYeKnFYzf/585eXl6b333lPp0qUlSW3atNGECRO0YMECxcTEyM/PTw0bNlRcXJxq1qypBx988IrHvnXrVu3atUtdu3bVY489JunSVLH3339f33zzjdq1a6fAwED7+kePHtX7779vb6tbt64GDx6s9evXX/GX+8uPfcyYMQ43zVepUkUTJ050mBL14IMP6pVXXtEPP/ygTp06XfG4LMvStGnTVLduXQ0bNsx+hSEmJkavvvqq5s+frzfeeKPImo4ePSpJqlixokN7rVq11K5dO02aNEnSpfF89dVXJV260btGjRq6++67r3rMFStWlGVZOnbsmCIiIq66PoA7F1csAOAmc/78eUmSl5dXsdbftm2bJKlDhw4O7R07dpSkAvdihISE2EOFJPu8/Hr16tlDhST7l8gTJ04U2OflX8JtNpvatWunnJwc7dy5s9AaLcvS5s2b1ahRI1mWpTNnztj/ioqK0rlz53To0KFiHe/ltm3bJhcXFz300EMO7R06dJBlWdq+fbtDe2RkpEPQCAsLk5eXV6HHWJjmzZsXeBKXm5ubPVTk5eXp999/l6enp4KDg3X48OGr9pmYmKjjx4/rgQce0O+//24/L1lZWapXr5727t1b4KrR5fKvtvj4+BRY9tRTT2nKlCkaPXq0pkyZonr16ikxMVFr165V7969de7cOU2aNEnPPfecYmNjlZSUVKAPX19fSdKZM2eueiwA7mxcsQCAm0x+oMgPGFeTkpIim83m8IVZkvz8/OTj46PU1FSH9svDgyR5e3tLkipUqFBoe2ZmpkO7zWZTpUqVHNqCgoLstRTmzJkzyszM1HfffafvvvuuyHWuVUpKisqVK1cghIWEhBRazx+P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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "if not summary_view.empty and summary_view[\"completion_rate_pct\"].notna().any():\n", " plot_df = summary_view[[\"task_name\", \"completion_rate_pct\"]].sort_values(\"completion_rate_pct\")\n", @@ -311,365 +189,10 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": null, "id": "694e0476", "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
task_namerun_idinterface_typecompletedduration_msvalidation_attemptsvalidation_successesvalidation_failuresquestionnaire_completedduration_s
0Coding Task1webTrue9014384183True90.14
1Coding Task2replFalse28458000False28.46
2Coding Task3replFalse10512000False10.51
3Coding Task4replFalse8732978078True87.33
4Coding Task5replFalse4171440040True41.71
5Coding Task6replFalse5493953053True54.94
6Coding Task7replFalse3962638038True39.63
7Coding Task8replFalse4816046046True48.16
8Coding Task9replFalse11927202True11.93
9Coding Task10replFalse258557202True258.56
10Coding Task11replFalse40334000False40.33
11Coding Task12replFalse21621000True21.62
12Coding Task13replFalse72097000True72.10
13Coding Task14replFalse9530000True9.53
14Coding Task15webFalse523000False0.52
15Coding Task16replFalse101759000True101.76
16Coding Task17replFalse114763000True114.76
17Coding Task18replFalse55497000True55.50
18Coding Task19replFalse263061000True263.06
\n", - "
" - ], - "text/plain": [ - " task_name run_id interface_type completed duration_ms \\\n", - "0 Coding Task 1 web True 90143 \n", - "1 Coding Task 2 repl False 28458 \n", - "2 Coding Task 3 repl False 10512 \n", - "3 Coding Task 4 repl False 87329 \n", - "4 Coding Task 5 repl False 41714 \n", - "5 Coding Task 6 repl False 54939 \n", - "6 Coding Task 7 repl False 39626 \n", - "7 Coding Task 8 repl False 48160 \n", - "8 Coding Task 9 repl False 11927 \n", - "9 Coding Task 10 repl False 258557 \n", - "10 Coding Task 11 repl False 40334 \n", - "11 Coding Task 12 repl False 21621 \n", - "12 Coding Task 13 repl False 72097 \n", - "13 Coding Task 14 repl False 9530 \n", - "14 Coding Task 15 web False 523 \n", - "15 Coding Task 16 repl False 101759 \n", - "16 Coding Task 17 repl False 114763 \n", - "17 Coding Task 18 repl False 55497 \n", - "18 Coding Task 19 repl False 263061 \n", - "\n", - " validation_attempts validation_successes validation_failures \\\n", - "0 84 1 83 \n", - "1 0 0 0 \n", - "2 0 0 0 \n", - "3 78 0 78 \n", - "4 40 0 40 \n", - "5 53 0 53 \n", - "6 38 0 38 \n", - "7 46 0 46 \n", - "8 2 0 2 \n", - "9 2 0 2 \n", - "10 0 0 0 \n", - "11 0 0 0 \n", - "12 0 0 0 \n", - "13 0 0 0 \n", - "14 0 0 0 \n", - "15 0 0 0 \n", - "16 0 0 0 \n", - "17 0 0 0 \n", - "18 0 0 0 \n", - "\n", - " questionnaire_completed duration_s \n", - "0 True 90.14 \n", - "1 False 28.46 \n", - "2 False 10.51 \n", - "3 True 87.33 \n", - "4 True 41.71 \n", - "5 True 54.94 \n", - "6 True 39.63 \n", - "7 True 48.16 \n", - "8 True 11.93 \n", - "9 True 258.56 \n", - "10 False 40.33 \n", - "11 True 21.62 \n", - "12 True 72.10 \n", - "13 True 9.53 \n", - "14 False 0.52 \n", - "15 True 101.76 \n", - "16 True 114.76 \n", - "17 True 55.50 \n", - "18 True 263.06 " - ] - }, - "execution_count": 8, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "run_cols = [\n", " \"task_name\",\n", @@ -692,76 +215,10 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": null, "id": "7e0aae07", "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
runscompleted_runsavg_duration_smedian_duration_savg_validation_attemptscompletion_rate_pct
task_name
Coding Task19171.0848.1618.055.3
\n", - "
" - ], - "text/plain": [ - " runs completed_runs avg_duration_s median_duration_s \\\n", - "task_name \n", - "Coding Task 19 1 71.08 48.16 \n", - "\n", - " avg_validation_attempts completion_rate_pct \n", - "task_name \n", - "Coding Task 18.05 5.3 " - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "if not runs_df.empty and {\"task_name\", \"run_id\", \"completed\", \"duration_ms\", \"validation_attempts\"}.issubset(runs_df.columns):\n", " agg = runs_df.groupby(\"task_name\", dropna=False).agg(\n", @@ -781,21 +238,10 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": null, "id": "25d3f1a6", "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAA90AAAHaCAYAAAAdVCoXAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjgsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvwVt1zgAAAAlwSFlzAAAPYQAAD2EBqD+naQAAXnZJREFUeJzt3X1cVGX+//H3DDPcKoyIBAqCiIimgpo3X3W9S8vKvKtV07Y2k7Y0c9vdWrftztLMLf3Wam1tauXWluZmZnm3llrepKaV95IiCgoC6qCICMOc3x9+mZ8TqEhMI/B6Ph4+ZM65zjmfcwYd3lznuo7JMAxDAAAAAACg2pm9XQAAAAAAALUVoRsAAAAAAA8hdAMAAAAA4CGEbgAAAAAAPITQDQAAAACAhxC6AQAAAADwEEI3AAAAAAAeQugGAAAAAMBDCN0AAAAAAHgIoRsAcM0zmUzq3bu3t8u4aunp6TKZTPrtb3/r7VIqraJr/eyzz8pkMmnt2rVeqemdd96RyWTSO++847Y8NjZWsbGxXqmpjLevTUV++9vfymQyKT093dul/KJq6v8TAGo/QjcA1EImk8ntj4+Pj0JDQ9W7d2+98847MgzD2yXWGvygf2U1OQReKvCjYjX5vQYAT7F4uwAAgOc888wzkqSSkhIdOHBAixcv1rp16/Ttt99q9uzZXq6u9mvSpIn27t2rkJAQb5fyszz88MMaOXKkmjZt6pXjDx06VF27dlVkZKRXjn853r42AIBrH6EbAGqxZ5991u31hg0b1LNnT73++uv64x//qGbNmnmnsDrCarUqMTHR22X8bGFhYQoLC/Pa8UNCQq7ZX1x4+9oAAK593F4OAHVI9+7dlZiYKMMwtG3bNtfytWvXymQylQvpZSoaO3vxbbdr1qxR7969Vb9+fQUHB+u2227T3r17r6q24uJiPf/882revLn8/PzUrFkzPfnkkzp//nyF7S93G+ulzqd3794ymUwqLi7Wc889p5YtW8rPz8815jo/P18vvfSS+vbtq6ioKPn6+qpRo0YaNGiQNm3aVOH5S9K6devcbucvO+7lxnRnZWVp/Pjxio2NdR1n2LBhbu/LT4/lrWt9qXHLX3/9tW6//XZFRUXJz89PERER6tq1qyZPnuxqYzKZ9O6770qSmjVr5rpGF38/Xel9udIt3vn5+Xr44YfVpEkT+fv7q3Xr1vr73/9ebhjF1X6f9+7dW/fdd58k6b777nN7j8u+7y43pvuLL77QgAEDFBoaKj8/PyUkJGjSpEnKz88v17bsGjgcDr3wwgtq0aKF/Pz8FB0drT//+c8qLi6usObLcTqdmjlzphITE+Xv76+oqCg9+uijOn36tKtNaWmpoqOjFRwcrIKCggr3M2HCBJlMJi1atOiyx6vMe71t2zZNnDhRSUlJCg0Nlb+/v1q0aKE//vGPOnXqVLl9FhcX6+9//7s6dOigBg0aKDAwULGxsRo8eLBWr15dqevw0ksvyWw2q3v37jp58mSltgGA6kRPNwDUUVartVr289lnn2nJkiW65ZZb9OCDD2rPnj1atmyZtm7dqj179lSqF9AwDA0fPlxLlixR8+bN9fDDD6u4uFjz5s3Tzp07q6XOi91xxx3aunWrbrnlFg0ZMkTh4eGSpL179+qvf/2revbsqdtuu00NGjTQkSNH9Omnn2r58uVaunSpBgwYIElKTk7WM888o8mTJysmJsYtWF9pjPehQ4fUo0cPHTt2TH379tVdd92ljIwMffTRR/r888/1n//8RwMHDiy33bV0rVesWKHbbrtNwcHBGjRokJo0aaKTJ09q7969ev31111DG5555hl98skn+uGHHzRx4kTZbDZJcv19sUu9L5dTXFysfv36yW63a+TIkSouLtZ//vMfTZw4Ufv379drr71W6XP6qd/+9rey2WxasmSJBg8erOTkZNe6iuq/2JtvvqmHHnpIQUFB+vWvf63w8HCtXbtW06dP19KlS7Vhw4YK9zFq1Ch9/fXXuuWWWxQcHKxly5bpb3/7m3JycvT2229fVf2PPvqovvrqKw0fPlyDBw/WypUr9corr+jrr7/W+vXr5e/vLx8fH6WkpOiZZ57RBx98oJSUFLd9nDt3Tu+9954iIiI0ePDgyx6vMu/1W2+9pcWLF6tXr17q16+fnE6ntm3bppkzZ2r58uXavHmz6tev72r/29/+Vh988IHatGmje+65RwEBATp27JjWr1+vFStWqF+/fpesx+l06ve//71mzZqlYcOG6f3335e/v/9VXUMAqBYGAKDWkWRU9F/8unXrDLPZbPj6+hrHjh1zLV+zZo0hyXjmmWcq3F9MTIwRExPjtuztt982JBk+Pj7G6tWr3dZNmjTJkGRMnz69UvW+//77hiSja9euxrlz51zLT5w4YcTFxRmSjF69erltc++99xqSjEOHDpXb36XOp1evXoYko23btkZubm657ex2e4XLMzIyjMjISCMxMbHcuopqK3Po0CFDknHvvfe6Lb/pppsMScaUKVPclm/YsMHw8fExQkNDjTNnzriWe/taP/PMM4YkY82aNa5lw4YNMyQZ33//fblj/PQaXu69Mowrvy9l5//222+7LY+JiTEkGd27dzeKiooqPJd169a5lv+c7/OfHrtMRdcmPT3d8PX1NerXr2/s3bvXrf1DDz1kSDJSUlIqvAYdOnQwTpw44VpeUFBgNG/e3DCbzUZWVlaFNfxU2fVu2LChkZ6e7lpeWlrqet+ee+451/Jjx44ZFovF6NixY7l9lZ3/E088cVXHvtR7nZ6ebjgcjnLL58yZY0gyXnzxRdcyu91umEwmo2PHjhVuk5eX5/b64u/dc+fOuc714YcfNkpLSytVPwB4AreXA0At9uyzz+rZZ5/VX//6V40YMUL9+vWTYRh6+eWXq21SqpEjR+rGG290W/bAAw9IkrZs2VKpfZT14L3wwgtuPVGhoaF66qmnqqXOiz3//PMV9gqHhIRUuDwqKkp33nmn9u3bpyNHjvysY2dmZmrVqlVq2rSpHn/8cbd13bp101133aWTJ0/q448/LrfttXitAwICyi2r6hjnS70vVzJt2jT5+fm5Xl98LlfbO1wd3nvvPRUXF+vhhx8uN6Z/6tSpql+/vv71r39VeDv/9OnTFRoa6nodFBSk0aNHy+l06ttvv72qOiZOnKiYmBjXa7PZ7LrVet68ea7lkZGRGjJkiLZt21ZueMObb74ps9lcrge8qmJiYuTj41Nu+ZgxYxQcHKyVK1e6lplMJhmGIT8/P5nN5X9kbdiwYYXHOHnypPr166fFixdr+vTpmjVrVoXbA8Avhf+BAKAWmzx5siZPnqwXXnhBCxculMPh0Ny5czVhwoRqO8YNN9xQbll0dLQkVThGsyLbt2+X2WxWjx49yq3zxOO4OnfufMl1GzZs0PDhwxUdHS0/Pz/XuNRZs2ZJko4ePfqzjv3dd99Jkn71q19VeIt/37593dpd7Fq61qNHj5YkdenSRQ8++KAWLFigzMzMSm9fkcu9L5disVjUrVu3csvLzqWi6+hp27dvl/T/38uLNWjQQO3bt1dRUZH27dtXbn11vMdlevXqVW5ZXFycoqOjlZ6eLrvd7lo+btw4SRdCdpmdO3fqm2++0c0331xtz0MvKSnR7Nmz1aNHD4WGhsrHx0cmk0lms1mnT592+/cVHBys22+/XRs3blRycrKee+45rVmzRoWFhZfc//Hjx9W9e3dt3bpV7733XrlfbAGANzCmGwBqMeP/JpI6e/asNm3apPvvv18PPvigYmJiKgwEVVHRuFSL5cLHS2lpaaX2kZ+fr9DQ0ApDaERExM+qryKX2ufixYt15513yt/fX/3791fz5s0VFBQks9mstWvXat26dZecbKyyyibRutSdBmXLLw5EZa6laz1s2DB99tlnmjFjhubNm+cKax07dtS0adPUv3//Su+rKscvExYWVmHPadm+Kpq0zNO8/R6Xue666ypcHhERocOHDys/P991vD59+qhVq1b64IMPNGPGDNWvX1///Oc/JUm/+93vruq4lzNixAgtXrxYcXFxGjx4sCIiIlx3Kbzyyivl/n0tWLBA06dP17///W/XPAH+/v6688479fLLL5c7x+zsbJ0+fVpRUVEV/mIJALyBnm4AqAOCgoLUr18/LV26VKWlpbr33nvdeovKbr10OBwVbl9ROKhOISEhOnnypEpKSsqty87OrnCby9V8pXrLZh3/qaeeekq+vr769ttv9cknn2jGjBl67rnn9Oyzz6ply5ZXOIvKKXv01aXOKysry61ddavKtb6U2267TV9++aVOnTqlL774Qo8++qh2796tgQMHas+ePVdd26Xel8vJy8urMIyWncvF1/GX+j739ntc5vjx4xUur+jaSNKDDz6ogoICvf/++64J1Jo0aVLhpH5V8e2332rx4sXq16+f9u/fr7ffflvTpk3Ts88+q6effrrCGdoDAgL07LPPKjU1VUeOHNF7772nHj166L333tOdd95Zrn1SUpLeffddHT16VD179lRaWlq11A4APwehGwDqkHbt2iklJUWZmZn63//9X9fyBg0aSJIyMjLKbXPgwAGP9xZ26NBBTqdT69evL7euokcxSZev+WrHvpY5cOCAWrdurVatWrktv1Rt0oUgdzU9kO3bt5ckrV+/vsLwt2bNGkkXroknVOVaX0lQUJD69u2rmTNn6oknnlBxcbGWL1/uWl/WE321PbWV4XA4tHHjxnLLy86l7HpLVfs+r0rtZces6Hra7XZ9//338vf3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X39db775piZOnChJKiws1JQpU9S2bVulpKToyJEj+sc//qGgoCD169dPkrR//369+uqrGjVqlDp06KD169frpZde0vTp09W0aVNJ0pIlS7R8+XKNHz9e4eHhWrBggaZOnaqZM2fK19fXE5cHAAAAAFDDVCp0p6amKiEhoUoHuJpt27dvr/bt21+2jcVikc1mq3BdZmamvv/+e02bNk3NmzeXdGHSt2nTpuk3v/mNQkNDtX79ejkcDo0bN04Wi0XR0dFKT0/XZ5995grdy5YtU3JysgYNGiRJGjlypHbu3KkVK1bogQcekGEYWrZsmYYNG6ZOnTpJkh5++GGlpKRo69at6t69e4X1lZSUuPVom0wmBQQEuL4GAKA2Kfts4zMOAFCXVSp0T548WQkJCerfv786duwoPz+/y7YvKirSt99+q//+9786ePCg3nvvvWopVrpwC/rYsWMVFBSkNm3aaOTIkapfv76kCwE/KCjIFbglqW3btjKZTDpw4IA6d+6s1NRUtWrVyq2HPCkpSUuWLFFBQYHq1aun1NRUDRw40O24SUlJ2rp1qyQpJydHdrtd7dq1c60PDAxUfHy8UlNTLxm6Fy9erEWLFrleN2vWTNOnT6/ULQkAANRUERER3i4BAACvqVTofvXVV7Vo0SLNnj1bPj4+atGihZo1a6bw8HAFBQVJujCWOicnR2lpaTpw4IBKS0vVq1cvTZgwodqKTU5OVpcuXRQeHq7s7Gx98MEHeuGFFzR16lSZzWbZ7XYFBwe7bePj46N69erJbrdLkux2u8LDw93alPWc2+12V9uQkBC3NiEhIW77KFt2qTYVGTp0qFuYL/vNf25urhwOR2UuAQAANYbJZFJERISys7MZ0w0AqHUsFkv1jekOCwvTgw8+qFGjRumrr77St99+q1WrVqm4uNitna+vr+Li4jRy5Ej17NmzXAD+uS7uQW7atKliYmI0YcIE7d69W23btq3WY3mC1WqV1WqtcB0/jAAAaivDMPicAwDUWVc1kVpwcLAGDhyogQMHqrS0VHl5eTpz5owkqX79+goLC5OPj49HCq3Iddddp/r16ys7O1tt27aVzWbT6dOn3dqUlpaqoKDA1Ztts9nK9UaXvb64TX5+vlub/Px8t/Vlyxo0aODWJjY2tjpODQAAAABQC5iruqGPj4+uu+46xcfHKz4+Xtddd90v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AACAhxC6AQAAAADwEEI3AACoVU6dOqWvvvpKJ0+elCQ5nU4vVwQAqMsI3QAAoFbZunWrFixYoNTUVLflx44dU3p6uneKAgDUWYRuAABQK5T1aHfu3FmhoaFKS0uTJJnNZtntdj3zzDM6fPiwN0sEANRBhG4AAFBjGYah0tJSSRfCtSTZbDZFR0fr8OHDyszMlCR99NFHat68uXr16uW1WgEAdROhGwAA1Fgmk0k+Pj6SpAMHDujQoUOSpOTkZJ08eVJpaWnKycnR9u3bNXr0aG+WCgCooyzeLgAAAOBihmHIMAxXz/XlnD59WosXL9b69evl5+enLl26qFmzZkpKStKyZcuUlpamQ4cOqUuXLmrSpImkC7ehV2bfAABUB0I3AADwuouDtslkkslkksPhUGFhoYKDgy+53dKlS5Wenq6UlBS1atVKJ06ckMPhkJ+fn1q2bKmNGzcqJydHbdu21YEDB5SQkCCz2UzwBgD8Yvi0AQAAXuN0OmUYhkwmkysEb9myRU899ZQmTJigY8eOudru3r1beXl5MgxDklRUVKR169ape/fu6ty5s/z8/BQbGyuL5UKfQocOHRQYGKguXbooIiJCL7zwgmbOnKk9e/YQuAEAvxiTUfbJBQAA4CUHDhzQl19+qc2bN8tisahbt27q16+frFarFi1apC1btigoKEi+vr761a9+pWHDhqmoqEizZ89Wdna24uLi5O/vL4fDIYvForvvvlu+vr6aNm2a6tevr9/97nfKycnRggULlJaWpvr16+v+++9XfHy8t08dAFDLEboBAIDXzJ8/X19//bWKiorUtGlTpaWl6ZlnnlFiYqKcTqc++eQTpaWlaciQIYqJidHmzZu1aNEi3XnnnerRo4dOnjypBQsWyGKxqH79+jpz5ow2b96szp0764EHHtDnn3+ujRs3atiwYerYsaMkKSMjQ2fOnFHr1q29fPYAgLqAMd0AAOAXVzam2mw266677lKfPn107tw5zZgxQ6tWrVJiYqKKi4vVtm1b9e3bVzabTdnZ2Tp69KiOHz+u77//Xu3atVNoaKgeeughSVJxcbF8fX1lMpm0f/9+SVKbNm20du1anTt3znXs6Ohor5wzAKBuoqcbAAD84srGcV+spKREy5cv16effqq33nrLtf7UqVOaP3++du7cqYSEBPn7+ysjI0O/+c1v1K5dOxUW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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "if not runs_df.empty and {\"duration_ms\", \"task_name\"}.issubset(runs_df.columns):\n", " runs_df.boxplot(column=\"duration_ms\", by=\"task_name\", figsize=(10, 5), rot=20)\n", @@ -817,131 +263,10 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": null, "id": "7e75fb47", "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
task_nametargetfailed_count
8Coding TaskThe original issue should be assigned to 'Me'....274
4Coding TaskNo new issues created273
5Coding TaskNo new issues created. Please create an issue ...273
10Coding TaskThe original issue should be marked as In Prog...255
6Coding TaskThe go.mod file should still contain the old v...57
0Coding TaskIssue should be assigned to 'Me', but was assi...39
2Coding TaskIssue should be set to Closed once the work is...39
1Coding TaskIssue should be marked as in-progress when wor...30
3Coding TaskIssue title does not match the expected value ...30
7Coding TaskThe original issue should be assigned to 'Me'.4
\n", - "
" - ], - "text/plain": [ - " task_name target \\\n", - "8 Coding Task The original issue should be assigned to 'Me'.... \n", - "4 Coding Task No new issues created \n", - "5 Coding Task No new issues created. Please create an issue ... \n", - "10 Coding Task The original issue should be marked as In Prog... \n", - "6 Coding Task The go.mod file should still contain the old v... \n", - "0 Coding Task Issue should be assigned to 'Me', but was assi... \n", - "2 Coding Task Issue should be set to Closed once the work is... \n", - "1 Coding Task Issue should be marked as in-progress when wor... \n", - "3 Coding Task Issue title does not match the expected value ... \n", - "7 Coding Task The original issue should be assigned to 'Me'. \n", - "\n", - " failed_count \n", - "8 274 \n", - "4 273 \n", - "5 273 \n", - "10 255 \n", - "6 57 \n", - "0 39 \n", - "2 39 \n", - "1 30 \n", - "3 30 \n", - "7 4 " - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "if not logs_df.empty and {\"action\", \"result\", \"task_name\", \"target\"}.issubset(logs_df.columns):\n", " validation_checks = logs_df[(logs_df[\"action\"] == \"validate_check\") & (logs_df[\"result\"] == \"failed\")].copy()\n", @@ -963,115 +288,10 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": null, "id": "e44f3e85", "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
task_namerun_idattempt_count
0Coding Task184
1Coding Task478
2Coding Task540
3Coding Task653
4Coding Task738
5Coding Task846
6Coding Task92
7Coding Task102
\n", - "
" - ], - "text/plain": [ - " task_name run_id attempt_count\n", - "0 Coding Task 1 84\n", - "1 Coding Task 4 78\n", - "2 Coding Task 5 40\n", - "3 Coding Task 6 53\n", - "4 Coding Task 7 38\n", - "5 Coding Task 8 46\n", - "6 Coding Task 9 2\n", - "7 Coding Task 10 2" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "image/png": "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W1RUVJbjMjIyZN++fSUAuX//foN9/fr1kwDk5cuX9dsuX74sAch+/foZHNuyZUsJQH777bcG21esWCEBZJvz4sWLUlXVLJk++eQTCUAuXrzYYPuYMWMkALlt27Ys5+SUd+/evRKALF68uIyOjtZvT0tLk+3bt5cA5MSJEw2uU6JECQlAtmnTRiYlJem3//fff9LT01N6enrK1NTUbDM86cqVKzI9PT3L9lmzZkkA8osvvjDYPnfu3Bw/UymNfw8mT55ssP3BgwfyxRdflEIIeezYMf32bdu26T+fhQsXGpwzcOBACUB6e3vLCRMmGOwbP358tp935vP7+vrKK1eu6LdnZGTIrl27SgBy/PjxBueUKFFClihRItvXk3nO8ePHs+yLjY3N9pwnPf4af/jhB4N9me0zJCREZmRk6Ldnvs/Dhw83+PzS09P178uKFSv02zN/LwDIjz76yKhcmTI/cyGEXLduXZb9Of3OZWrcuLF88j8HH3/NT7aln376SQKQb7755nPlJCIqyHh7ORFRATVkyBAAWSdUi46Oxtq1a+Hv749OnTrpt5cpUybLNRRFwYgRIwAAGzZsMCnHv//+i02bNqFUqVIYPny4wb5OnTqhcePG2Z5XunTpLLfFArpe0tzkedycOXMAAJ988gkCAgL02+3t7TFlyhQoipLjhHTff/89XFxc9I8z38/4+HicP3/eqOcvUaKEvrf+cQMHDkShQoXM8hoz3b59GwsXLkTNmjUxatQog33Ozs76W+MXLVqU5dzw8HD06dPHYFu/fv0AAJ6envjwww8N9vXt2xcAcPz48WyzjBgxAiVKlNA/VhQFX331FRRF0X8mz+PxzyGTn5/fc10jJCQEQ4cONdiW2T6joqKwa9cuAICqqvjhhx8QEBCAb775xuDzs7Ozw5QpUyCEwG+//ZblOYoUKfJcve9PZmndurVJ5+akQYMGWW5LHzhwIOzt7XHw4EGzPhcRkS3j7eVERAVUs2bNUKZMGezZswdnz55F+fLlAQBz585Feno6+vfvbzCp2O3bt/HVV19h7dq1uHTpEhITEw2ud/36dZNyHDt2DICucMuuwGzSpAl27NiRZXtiYiK+++47LF++HJGRkbh3757BeGlT8zwuc6K5Zs2aZdkXGhqKoKAgXL58GfHx8fD09NTv8/T0REhISJZzihcvDgBZxmPnJC0t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lNdHAY5OwNkTkOv/1DoSERERkVVi0U1ERNkSgcUher8OAJArf4OMOqtxIiIiIiLrw6KbiIhyJOo3h6jdGFBV3TJiife0jkRERERkVVh0ExFRjoQQEK++CfgHAndiof76A6SUWsciIiIishosuomI6KmEsyuUIe8DdvbAsf2Q29dqHYmIiIjIarDoJiKiZxIlQiC69wMAyKWzIa9d0jgRERERkXVg0U1EREYRzTsClWsB6elQf/kKMvmB1pGIiIiI8j0W3UREZBQhBJT+IwAvX+C/65CLftY6EhEREVG+x6KbiIiMJjwKQRn8P0AokPu2Qt23TetIRERERPkai24iInouIrQiRIdeAAD52wzIm9c1TkRERESUf7HoJiKi5ybavQSUqwSkJEP95UvItDStIxERERHlSyy6iYjouQnFDsqgdwH3QsA/lyGXzdU6EhEREVG+xKKbiIhMIrx8oQx8BwAgt66BPL5f20BERERE+RCLbiIiMpmoVBOiVWcAgDr3e8jbsdoGIiIiIspnWHQTEVGuiC6vAiXLAkn3oc76GjIjQ+tIRERERPkGi24iIsoVYe8AZcj7gIsrEHUWctXvWkciIiIiyjdYdBMRUa6JwgEQrw4DAMh1f0CePaFxIiIiIqL8gUU3ERGZhVKrIUTDVoCUUGdPhUyI0zoSERERkeZYdBMRkdmInoOBosFA/F2oc76BVFWtIxERERFpyqSi+8GDB7h165bBtjt37mDJkiVYuHAhoqKizBKOiIisi3BygjJkFODoCJw+BrlphdaRiIiIiDRlUtH9888/45tvvtE/TkpKwscff4yIiAisWbMGY8aMwenTp80WkoiIrIcoFqzr8QYgly+AvHhO40RERERE2jGp6D5//jyqV6+uf7xr1y7cvXsXn3/+OebOnYvg4GBERESYLSQREVkX0bAVRK2GQEYG1JlfQybd1zoSERERkSZMKroTEhLg4+Ojf3z48GGEhYUhNDQULi4uaNy4Ma5cuWKujEREZGWEEBCvDAX8igC3Y6DOnwYppdaxiIiIiPKcSUW3m5sb4uLiAACpqak4d+4cKleu/OiiioLU1FSzBCQiIuskXN1063fb2QFH9kLu3KB1JCIiIqI8Z1LRHRoaio0bN+LgwYOYN28eUlNTUatWLf3+6Ohog55wIiIqmESpUIiufQEAcsksyH+vaBuIiIiI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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "if not logs_df.empty and {\"action\", \"task_name\", \"run_id\"}.issubset(logs_df.columns):\n", " attempt_logs = logs_df[logs_df[\"action\"] == \"validate_attempt\"].copy()\n", @@ -1112,126 +332,10 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": null, "id": "6f82ba83", "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
task_nametargetcount
10Coding TaskNaN35
0Coding TaskGET /9
5Coding TaskGET /status/modal5
3Coding TaskGET /issues/pm-2vr/edit2
6Coding TaskPATCH /issues/pm-2vr2
1Coding TaskGET /issues/create1
2Coding TaskGET /issues/pm-2vr/assignee1
4Coding TaskGET /issues/pm-c9p/assignee1
7Coding TaskPATCH /issues/pm-2vr/assignee1
8Coding TaskPATCH /issues/pm-c9p/assignee1
9Coding TaskPOST /issues1
\n", - "
" - ], - "text/plain": [ - " task_name target count\n", - "10 Coding Task NaN 35\n", - "0 Coding Task GET / 9\n", - "5 Coding Task GET /status/modal 5\n", - "3 Coding Task GET /issues/pm-2vr/edit 2\n", - "6 Coding Task PATCH /issues/pm-2vr 2\n", - "1 Coding Task GET /issues/create 1\n", - "2 Coding Task GET /issues/pm-2vr/assignee 1\n", - "4 Coding Task GET /issues/pm-c9p/assignee 1\n", - "7 Coding Task PATCH /issues/pm-2vr/assignee 1\n", - "8 Coding Task PATCH /issues/pm-c9p/assignee 1\n", - "9 Coding Task POST /issues 1" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "if not logs_df.empty and {\"level\", \"task_name\", \"target\"}.issubset(logs_df.columns):\n", " user_actions = logs_df[logs_df[\"level\"] == \"user_action\"].copy()\n", @@ -1262,1117 +366,10 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": null, "id": "f8502e4a", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Task: Coding Task, run_id: 1\n" - ] - }, - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
run_idtask_nameinterface_typestarted_atended_atduration_mscompletedvalidation_attemptsvalidation_successesvalidation_failuresvalidation_checks_passedvalidation_checks_failedquestionnaire_completedquestionnaire_user_quitquestionnaire_answerslogsfileerror
01Coding Taskweb2026-03-04 15:11:56.497145550+01:002026-03-04 15:13:26.641005636+01:0090143True84183499242TrueFalse{'final_confirmation': None, 'task_completed':...[{'timestamp': '2026-03-04T15:11:56.497159847+...coding-task-stats.jsonNaN
\n", - "
" - ], - "text/plain": [ - " run_id task_name interface_type started_at \\\n", - "0 1 Coding Task web 2026-03-04 15:11:56.497145550+01:00 \n", - "\n", - " ended_at duration_ms completed \\\n", - "0 2026-03-04 15:13:26.641005636+01:00 90143 True \n", - "\n", - " validation_attempts validation_successes validation_failures \\\n", - "0 84 1 83 \n", - "\n", - " validation_checks_passed validation_checks_failed \\\n", - "0 499 242 \n", - "\n", - " questionnaire_completed questionnaire_user_quit \\\n", - "0 True False \n", - "\n", - " questionnaire_answers \\\n", - "0 {'final_confirmation': None, 'task_completed':... \n", - "\n", - " logs file \\\n", - "0 [{'timestamp': '2026-03-04T15:11:56.497159847+... coding-task-stats.json \n", - "\n", - " error \n", - "0 NaN " - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
timestamplevelmessagesourceactionresulttask_namefilerun_idrun_completedtargetattempt
02026-03-04 15:11:56.497159847+01:00infotask run startedsystemtask_run_startedokCoding Taskcoding-task-stats.json1TrueNaNNaN
12026-03-04 15:11:57.463612409+01:00user_action{\"source\":\"web\",\"action\":\"request\",\"target\":\"G...webrequestokCoding Taskcoding-task-stats.json1TrueGET /NaN
22026-03-04 15:11:58.318154048+01:00validationvalidation attempt 1systemvalidate_attemptfailedCoding Taskcoding-task-stats.json1TrueNaN1.0
32026-03-04 15:11:58.318199703+01:00validationvalidation check: The original issue should be...systemvalidate_checkfailedCoding Taskcoding-task-stats.json1TrueThe original issue should be assigned to 'Me'....1.0
42026-03-04 15:11:58.318212096+01:00validationvalidation check: No new issues created. Pleas...systemvalidate_checkfailedCoding Taskcoding-task-stats.json1TrueNo new issues created. Please create an issue ...1.0
52026-03-04 15:11:58.318221323+01:00validationvalidation check: No new issues createdsystemvalidate_checkfailedCoding Taskcoding-task-stats.json1TrueNo new issues created1.0
62026-03-04 15:11:59.317079434+01:00validationvalidation attempt 2systemvalidate_attemptfailedCoding Taskcoding-task-stats.json1TrueNaN2.0
72026-03-04 15:11:59.317145086+01:00validationvalidation check: The original issue should be...systemvalidate_checkfailedCoding Taskcoding-task-stats.json1TrueThe original issue should be assigned to 'Me'....2.0
82026-03-04 15:11:59.317171636+01:00validationvalidation check: No new issues created. Pleas...systemvalidate_checkfailedCoding Taskcoding-task-stats.json1TrueNo new issues created. Please create an issue ...2.0
92026-03-04 15:11:59.317194609+01:00validationvalidation check: No new issues createdsystemvalidate_checkfailedCoding Taskcoding-task-stats.json1TrueNo new issues created2.0
102026-03-04 15:12:00.317912946+01:00validationvalidation attempt 3systemvalidate_attemptfailedCoding Taskcoding-task-stats.json1TrueNaN3.0
112026-03-04 15:12:00.317988177+01:00validationvalidation check: The original issue should be...systemvalidate_checkfailedCoding Taskcoding-task-stats.json1TrueThe original issue should be assigned to 'Me'....3.0
122026-03-04 15:12:00.318006912+01:00validationvalidation check: No new issues created. Pleas...systemvalidate_checkfailedCoding Taskcoding-task-stats.json1TrueNo new issues created. Please create an issue ...3.0
132026-03-04 15:12:00.318020798+01:00validationvalidation check: No new issues createdsystemvalidate_checkfailedCoding Taskcoding-task-stats.json1TrueNo new issues created3.0
142026-03-04 15:12:01.317868206+01:00validationvalidation attempt 4systemvalidate_attemptfailedCoding Taskcoding-task-stats.json1TrueNaN4.0
152026-03-04 15:12:01.317945661+01:00validationvalidation check: The original issue should be...systemvalidate_checkfailedCoding Taskcoding-task-stats.json1TrueThe original issue should be assigned to 'Me'....4.0
162026-03-04 15:12:01.317963825+01:00validationvalidation check: No new issues created. Pleas...systemvalidate_checkfailedCoding Taskcoding-task-stats.json1TrueNo new issues created. Please create an issue ...4.0
172026-03-04 15:12:01.317977450+01:00validationvalidation check: No new issues createdsystemvalidate_checkfailedCoding Taskcoding-task-stats.json1TrueNo new issues created4.0
182026-03-04 15:12:01.570644963+01:00user_action{\"source\":\"web\",\"action\":\"request\",\"target\":\"G...webrequestokCoding Taskcoding-task-stats.json1TrueGET /issues/createNaN
192026-03-04 15:12:02.317463062+01:00validationvalidation attempt 5systemvalidate_attemptfailedCoding Taskcoding-task-stats.json1TrueNaN5.0
202026-03-04 15:12:02.317511663+01:00validationvalidation check: The original issue should be...systemvalidate_checkfailedCoding Taskcoding-task-stats.json1TrueThe original issue should be assigned to 'Me'....5.0
212026-03-04 15:12:02.317527422+01:00validationvalidation check: No new issues created. Pleas...systemvalidate_checkfailedCoding Taskcoding-task-stats.json1TrueNo new issues created. Please create an issue ...5.0
222026-03-04 15:12:02.317538603+01:00validationvalidation check: No new issues createdsystemvalidate_checkfailedCoding Taskcoding-task-stats.json1TrueNo new issues created5.0
232026-03-04 15:12:03.317011327+01:00validationvalidation attempt 6systemvalidate_attemptfailedCoding Taskcoding-task-stats.json1TrueNaN6.0
242026-03-04 15:12:03.317060369+01:00validationvalidation check: The original issue should be...systemvalidate_checkfailedCoding Taskcoding-task-stats.json1TrueThe original issue should be assigned to 'Me'....6.0
252026-03-04 15:12:03.317072492+01:00validationvalidation check: No new issues created. Pleas...systemvalidate_checkfailedCoding Taskcoding-task-stats.json1TrueNo new issues created. Please create an issue ...6.0
262026-03-04 15:12:03.317081078+01:00validationvalidation check: No new issues createdsystemvalidate_checkfailedCoding Taskcoding-task-stats.json1TrueNo new issues created6.0
272026-03-04 15:12:04.317413612+01:00validationvalidation attempt 7systemvalidate_attemptfailedCoding Taskcoding-task-stats.json1TrueNaN7.0
282026-03-04 15:12:04.317467062+01:00validationvalidation check: The original issue should be...systemvalidate_checkfailedCoding Taskcoding-task-stats.json1TrueThe original issue should be assigned to 'Me'....7.0
292026-03-04 15:12:04.317479285+01:00validationvalidation check: No new issues created. Pleas...systemvalidate_checkfailedCoding Taskcoding-task-stats.json1TrueNo new issues created. Please create an issue ...7.0
302026-03-04 15:12:04.317488011+01:00validationvalidation check: No new issues createdsystemvalidate_checkfailedCoding Taskcoding-task-stats.json1TrueNo new issues created7.0
312026-03-04 15:12:05.317465258+01:00validationvalidation attempt 8systemvalidate_attemptfailedCoding Taskcoding-task-stats.json1TrueNaN8.0
322026-03-04 15:12:05.317554294+01:00validationvalidation check: The original issue should be...systemvalidate_checkfailedCoding Taskcoding-task-stats.json1TrueThe original issue should be assigned to 'Me'....8.0
332026-03-04 15:12:05.317579081+01:00validationvalidation check: No new issues created. Pleas...systemvalidate_checkfailedCoding Taskcoding-task-stats.json1TrueNo new issues created. Please create an issue ...8.0
342026-03-04 15:12:05.317597465+01:00validationvalidation check: No new issues createdsystemvalidate_checkfailedCoding Taskcoding-task-stats.json1TrueNo new issues created8.0
352026-03-04 15:12:06.317295761+01:00validationvalidation attempt 9systemvalidate_attemptfailedCoding Taskcoding-task-stats.json1TrueNaN9.0
362026-03-04 15:12:06.317375340+01:00validationvalidation check: The original issue should be...systemvalidate_checkfailedCoding Taskcoding-task-stats.json1TrueThe original issue should be assigned to 'Me'....9.0
372026-03-04 15:12:06.317405787+01:00validationvalidation check: No new issues created. Pleas...systemvalidate_checkfailedCoding Taskcoding-task-stats.json1TrueNo new issues created. Please create an issue ...9.0
382026-03-04 15:12:06.317434641+01:00validationvalidation check: No new issues createdsystemvalidate_checkfailedCoding Taskcoding-task-stats.json1TrueNo new issues created9.0
392026-03-04 15:12:07.317542913+01:00validationvalidation attempt 10systemvalidate_attemptfailedCoding Taskcoding-task-stats.json1TrueNaN10.0
402026-03-04 15:12:07.317613084+01:00validationvalidation check: The original issue should be...systemvalidate_checkfailedCoding Taskcoding-task-stats.json1TrueThe original issue should be assigned to 'Me'....10.0
412026-03-04 15:12:07.317626178+01:00validationvalidation check: No new issues created. Pleas...systemvalidate_checkfailedCoding Taskcoding-task-stats.json1TrueNo new issues created. Please create an issue ...10.0
422026-03-04 15:12:07.317635035+01:00validationvalidation check: No new issues createdsystemvalidate_checkfailedCoding Taskcoding-task-stats.json1TrueNo new issues created10.0
432026-03-04 15:12:08.317693562+01:00validationvalidation attempt 11systemvalidate_attemptfailedCoding Taskcoding-task-stats.json1TrueNaN11.0
442026-03-04 15:12:08.317760387+01:00validationvalidation check: The original issue should be...systemvalidate_checkfailedCoding Taskcoding-task-stats.json1TrueThe original issue should be assigned to 'Me'....11.0
452026-03-04 15:12:08.317773101+01:00validationvalidation check: No new issues created. Pleas...systemvalidate_checkfailedCoding Taskcoding-task-stats.json1TrueNo new issues created. Please create an issue ...11.0
462026-03-04 15:12:08.317782208+01:00validationvalidation check: No new issues createdsystemvalidate_checkfailedCoding Taskcoding-task-stats.json1TrueNo new issues created11.0
472026-03-04 15:12:09.317310231+01:00validationvalidation attempt 12systemvalidate_attemptfailedCoding Taskcoding-task-stats.json1TrueNaN12.0
482026-03-04 15:12:09.317473836+01:00validationvalidation check: The original issue should be...systemvalidate_checkfailedCoding Taskcoding-task-stats.json1TrueThe original issue should be assigned to 'Me'....12.0
492026-03-04 15:12:09.317512218+01:00validationvalidation check: No new issues created. Pleas...systemvalidate_checkfailedCoding Taskcoding-task-stats.json1TrueNo new issues created. Please create an issue ...12.0
\n", - "
" - ], - "text/plain": [ - " timestamp level \\\n", - "0 2026-03-04 15:11:56.497159847+01:00 info \n", - "1 2026-03-04 15:11:57.463612409+01:00 user_action \n", - "2 2026-03-04 15:11:58.318154048+01:00 validation \n", - "3 2026-03-04 15:11:58.318199703+01:00 validation \n", - "4 2026-03-04 15:11:58.318212096+01:00 validation \n", - "5 2026-03-04 15:11:58.318221323+01:00 validation \n", - "6 2026-03-04 15:11:59.317079434+01:00 validation \n", - "7 2026-03-04 15:11:59.317145086+01:00 validation \n", - "8 2026-03-04 15:11:59.317171636+01:00 validation \n", - "9 2026-03-04 15:11:59.317194609+01:00 validation \n", - "10 2026-03-04 15:12:00.317912946+01:00 validation \n", - "11 2026-03-04 15:12:00.317988177+01:00 validation \n", - "12 2026-03-04 15:12:00.318006912+01:00 validation \n", - "13 2026-03-04 15:12:00.318020798+01:00 validation \n", - "14 2026-03-04 15:12:01.317868206+01:00 validation \n", - "15 2026-03-04 15:12:01.317945661+01:00 validation \n", - "16 2026-03-04 15:12:01.317963825+01:00 validation \n", - "17 2026-03-04 15:12:01.317977450+01:00 validation \n", - "18 2026-03-04 15:12:01.570644963+01:00 user_action \n", - "19 2026-03-04 15:12:02.317463062+01:00 validation \n", - "20 2026-03-04 15:12:02.317511663+01:00 validation \n", - "21 2026-03-04 15:12:02.317527422+01:00 validation \n", - "22 2026-03-04 15:12:02.317538603+01:00 validation \n", - "23 2026-03-04 15:12:03.317011327+01:00 validation \n", - "24 2026-03-04 15:12:03.317060369+01:00 validation \n", - "25 2026-03-04 15:12:03.317072492+01:00 validation \n", - "26 2026-03-04 15:12:03.317081078+01:00 validation \n", - "27 2026-03-04 15:12:04.317413612+01:00 validation \n", - "28 2026-03-04 15:12:04.317467062+01:00 validation \n", - "29 2026-03-04 15:12:04.317479285+01:00 validation \n", - "30 2026-03-04 15:12:04.317488011+01:00 validation \n", - "31 2026-03-04 15:12:05.317465258+01:00 validation \n", - "32 2026-03-04 15:12:05.317554294+01:00 validation \n", - "33 2026-03-04 15:12:05.317579081+01:00 validation \n", - "34 2026-03-04 15:12:05.317597465+01:00 validation \n", - "35 2026-03-04 15:12:06.317295761+01:00 validation \n", - "36 2026-03-04 15:12:06.317375340+01:00 validation \n", - "37 2026-03-04 15:12:06.317405787+01:00 validation \n", - "38 2026-03-04 15:12:06.317434641+01:00 validation \n", - "39 2026-03-04 15:12:07.317542913+01:00 validation \n", - "40 2026-03-04 15:12:07.317613084+01:00 validation \n", - "41 2026-03-04 15:12:07.317626178+01:00 validation \n", - "42 2026-03-04 15:12:07.317635035+01:00 validation \n", - "43 2026-03-04 15:12:08.317693562+01:00 validation \n", - "44 2026-03-04 15:12:08.317760387+01:00 validation \n", - "45 2026-03-04 15:12:08.317773101+01:00 validation \n", - "46 2026-03-04 15:12:08.317782208+01:00 validation \n", - "47 2026-03-04 15:12:09.317310231+01:00 validation \n", - "48 2026-03-04 15:12:09.317473836+01:00 validation \n", - "49 2026-03-04 15:12:09.317512218+01:00 validation \n", - "\n", - " message source \\\n", - "0 task run started system \n", - "1 {\"source\":\"web\",\"action\":\"request\",\"target\":\"G... web \n", - "2 validation attempt 1 system \n", - "3 validation check: The original issue should be... system \n", - "4 validation check: No new issues created. Pleas... system \n", - "5 validation check: No new issues created system \n", - "6 validation attempt 2 system \n", - "7 validation check: The original issue should be... system \n", - "8 validation check: No new issues created. Pleas... system \n", - "9 validation check: No new issues created system \n", - "10 validation attempt 3 system \n", - "11 validation check: The original issue should be... system \n", - "12 validation check: No new issues created. Pleas... system \n", - "13 validation check: No new issues created system \n", - "14 validation attempt 4 system \n", - "15 validation check: The original issue should be... system \n", - "16 validation check: No new issues created. Pleas... system \n", - "17 validation check: No new issues created system \n", - "18 {\"source\":\"web\",\"action\":\"request\",\"target\":\"G... web \n", - "19 validation attempt 5 system \n", - "20 validation check: The original issue should be... system \n", - "21 validation check: No new issues created. Pleas... system \n", - "22 validation check: No new issues created system \n", - "23 validation attempt 6 system \n", - "24 validation check: The original issue should be... system \n", - "25 validation check: No new issues created. Pleas... system \n", - "26 validation check: No new issues created system \n", - "27 validation attempt 7 system \n", - "28 validation check: The original issue should be... system \n", - "29 validation check: No new issues created. Pleas... system \n", - "30 validation check: No new issues created system \n", - "31 validation attempt 8 system \n", - "32 validation check: The original issue should be... system \n", - "33 validation check: No new issues created. Pleas... system \n", - "34 validation check: No new issues created system \n", - "35 validation attempt 9 system \n", - "36 validation check: The original issue should be... system \n", - "37 validation check: No new issues created. Pleas... system \n", - "38 validation check: No new issues created system \n", - "39 validation attempt 10 system \n", - "40 validation check: The original issue should be... system \n", - "41 validation check: No new issues created. Pleas... system \n", - "42 validation check: No new issues created system \n", - "43 validation attempt 11 system \n", - "44 validation check: The original issue should be... system \n", - "45 validation check: No new issues created. Pleas... system \n", - "46 validation check: No new issues created system \n", - "47 validation attempt 12 system \n", - "48 validation check: The original issue should be... system \n", - "49 validation check: No new issues created. Pleas... system \n", - "\n", - " action result task_name file run_id \\\n", - "0 task_run_started ok Coding Task coding-task-stats.json 1 \n", - "1 request ok Coding Task coding-task-stats.json 1 \n", - "2 validate_attempt failed Coding Task coding-task-stats.json 1 \n", - "3 validate_check failed Coding Task coding-task-stats.json 1 \n", - "4 validate_check failed Coding Task coding-task-stats.json 1 \n", - "5 validate_check failed Coding Task coding-task-stats.json 1 \n", - "6 validate_attempt failed Coding Task coding-task-stats.json 1 \n", - "7 validate_check failed Coding Task coding-task-stats.json 1 \n", - "8 validate_check failed Coding Task coding-task-stats.json 1 \n", - "9 validate_check failed Coding Task coding-task-stats.json 1 \n", - "10 validate_attempt failed Coding Task coding-task-stats.json 1 \n", - "11 validate_check failed Coding Task coding-task-stats.json 1 \n", - "12 validate_check failed Coding Task coding-task-stats.json 1 \n", - "13 validate_check failed Coding Task coding-task-stats.json 1 \n", - "14 validate_attempt failed Coding Task coding-task-stats.json 1 \n", - "15 validate_check failed Coding Task coding-task-stats.json 1 \n", - "16 validate_check failed Coding Task coding-task-stats.json 1 \n", - "17 validate_check failed Coding Task coding-task-stats.json 1 \n", - "18 request ok Coding Task coding-task-stats.json 1 \n", - "19 validate_attempt failed Coding Task coding-task-stats.json 1 \n", - "20 validate_check failed Coding Task coding-task-stats.json 1 \n", - "21 validate_check failed Coding Task coding-task-stats.json 1 \n", - "22 validate_check failed Coding Task coding-task-stats.json 1 \n", - "23 validate_attempt failed Coding Task coding-task-stats.json 1 \n", - "24 validate_check failed Coding Task coding-task-stats.json 1 \n", - "25 validate_check failed Coding Task coding-task-stats.json 1 \n", - "26 validate_check failed Coding Task coding-task-stats.json 1 \n", - "27 validate_attempt failed Coding Task coding-task-stats.json 1 \n", - "28 validate_check failed Coding Task coding-task-stats.json 1 \n", - "29 validate_check failed Coding Task coding-task-stats.json 1 \n", - "30 validate_check failed Coding Task coding-task-stats.json 1 \n", - "31 validate_attempt failed Coding Task coding-task-stats.json 1 \n", - "32 validate_check failed Coding Task coding-task-stats.json 1 \n", - "33 validate_check failed Coding Task coding-task-stats.json 1 \n", - "34 validate_check failed Coding Task coding-task-stats.json 1 \n", - "35 validate_attempt failed Coding Task coding-task-stats.json 1 \n", - "36 validate_check failed Coding Task coding-task-stats.json 1 \n", - "37 validate_check failed Coding Task coding-task-stats.json 1 \n", - "38 validate_check failed Coding Task coding-task-stats.json 1 \n", - "39 validate_attempt failed Coding Task coding-task-stats.json 1 \n", - "40 validate_check failed Coding Task coding-task-stats.json 1 \n", - "41 validate_check failed Coding Task coding-task-stats.json 1 \n", - "42 validate_check failed Coding Task coding-task-stats.json 1 \n", - "43 validate_attempt failed Coding Task coding-task-stats.json 1 \n", - "44 validate_check failed Coding Task coding-task-stats.json 1 \n", - "45 validate_check failed Coding Task coding-task-stats.json 1 \n", - "46 validate_check failed Coding Task coding-task-stats.json 1 \n", - "47 validate_attempt failed Coding Task coding-task-stats.json 1 \n", - "48 validate_check failed Coding Task coding-task-stats.json 1 \n", - "49 validate_check failed Coding Task coding-task-stats.json 1 \n", - "\n", - " run_completed target attempt \n", - "0 True NaN NaN \n", - "1 True GET / NaN \n", - "2 True NaN 1.0 \n", - "3 True The original issue should be assigned to 'Me'.... 1.0 \n", - "4 True No new issues created. Please create an issue ... 1.0 \n", - "5 True No new issues created 1.0 \n", - "6 True NaN 2.0 \n", - "7 True The original issue should be assigned to 'Me'.... 2.0 \n", - "8 True No new issues created. Please create an issue ... 2.0 \n", - "9 True No new issues created 2.0 \n", - "10 True NaN 3.0 \n", - "11 True The original issue should be assigned to 'Me'.... 3.0 \n", - "12 True No new issues created. Please create an issue ... 3.0 \n", - "13 True No new issues created 3.0 \n", - "14 True NaN 4.0 \n", - "15 True The original issue should be assigned to 'Me'.... 4.0 \n", - "16 True No new issues created. Please create an issue ... 4.0 \n", - "17 True No new issues created 4.0 \n", - "18 True GET /issues/create NaN \n", - "19 True NaN 5.0 \n", - "20 True The original issue should be assigned to 'Me'.... 5.0 \n", - "21 True No new issues created. Please create an issue ... 5.0 \n", - "22 True No new issues created 5.0 \n", - "23 True NaN 6.0 \n", - "24 True The original issue should be assigned to 'Me'.... 6.0 \n", - "25 True No new issues created. Please create an issue ... 6.0 \n", - "26 True No new issues created 6.0 \n", - "27 True NaN 7.0 \n", - "28 True The original issue should be assigned to 'Me'.... 7.0 \n", - "29 True No new issues created. Please create an issue ... 7.0 \n", - "30 True No new issues created 7.0 \n", - "31 True NaN 8.0 \n", - "32 True The original issue should be assigned to 'Me'.... 8.0 \n", - "33 True No new issues created. Please create an issue ... 8.0 \n", - "34 True No new issues created 8.0 \n", - "35 True NaN 9.0 \n", - "36 True The original issue should be assigned to 'Me'.... 9.0 \n", - "37 True No new issues created. Please create an issue ... 9.0 \n", - "38 True No new issues created 9.0 \n", - "39 True NaN 10.0 \n", - "40 True The original issue should be assigned to 'Me'.... 10.0 \n", - "41 True No new issues created. Please create an issue ... 10.0 \n", - "42 True No new issues created 10.0 \n", - "43 True NaN 11.0 \n", - "44 True The original issue should be assigned to 'Me'.... 11.0 \n", - "45 True No new issues created. Please create an issue ... 11.0 \n", - "46 True No new issues created 11.0 \n", - "47 True NaN 12.0 \n", - "48 True The original issue should be assigned to 'Me'.... 12.0 \n", - "49 True No new issues created. Please create an issue ... 12.0 " - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "# Example: inspect one task and one run.\n", "if not runs_df.empty and {\"task_name\", \"run_id\"}.issubset(runs_df.columns):\n", From 372e5900ff6b56e4a8bf390b22995e1c37752d85 Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Thu, 5 Mar 2026 12:30:50 +0100 Subject: [PATCH 49/51] wait little for submitt channel to update --- pkg/web/handler/task.go | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/pkg/web/handler/task.go b/pkg/web/handler/task.go index dab8086..08984bb 100644 --- a/pkg/web/handler/task.go +++ b/pkg/web/handler/task.go @@ -4,6 +4,7 @@ import ( "context" "io" "net/http" + "time" "github.com/LazyBachelor/LazyPM/internal/models" "github.com/LazyBachelor/LazyPM/pkg/web/components" @@ -24,12 +25,8 @@ func SetSubmitChan(ch chan<- struct{}) { } func HandleTaskStatus(w http.ResponseWriter, r *http.Request) { - if submitChan != nil { - select { - case submitChan <- struct{}{}: - default: - } - } + submitChan <- struct{}{} + time.Sleep(100 * time.Millisecond) hx := HTMX(r) if hx.IsHxRequest() { From d7c52383bedd7697ee7c00082f6f26bd0ddb6bf6 Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Thu, 5 Mar 2026 12:33:16 +0100 Subject: [PATCH 50/51] add submit to full help tui --- pkg/tui/views/dashboard/help_bar.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/tui/views/dashboard/help_bar.go b/pkg/tui/views/dashboard/help_bar.go index f0bb51e..39a1949 100644 --- a/pkg/tui/views/dashboard/help_bar.go +++ b/pkg/tui/views/dashboard/help_bar.go @@ -79,7 +79,7 @@ func (h HelpBar) fullHelp() string { renderRow("c", "add comment", "", ""), renderRow("s", "change status", "p", "change priority"), renderRow("t", "change type", "x", "delete issue"), - renderRow("?", "help", "q", "quit"), + renderRow("S", "submit task", "q", "quit"), } content := lipgloss.JoinVertical(lipgloss.Left, rows...) return lipgloss.NewStyle(). From f371f9a8e9cc132191787871077d8110d82dea42 Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Thu, 5 Mar 2026 14:03:55 +0100 Subject: [PATCH 51/51] fix submisson button for web --- pkg/web/components/status.templ | 4 ++-- pkg/web/components/status_templ.go | 2 +- pkg/web/handler/task.go | 9 +++++++-- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/pkg/web/components/status.templ b/pkg/web/components/status.templ index 63ea2de..ad255c0 100644 --- a/pkg/web/components/status.templ +++ b/pkg/web/components/status.templ @@ -1,7 +1,7 @@ package components templ Status() { -
-
+
+
} diff --git a/pkg/web/components/status_templ.go b/pkg/web/components/status_templ.go index 0a2f7e1..451a387 100644 --- a/pkg/web/components/status_templ.go +++ b/pkg/web/components/status_templ.go @@ -29,7 +29,7 @@ func Status() templ.Component { templ_7745c5c3_Var1 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/pkg/web/handler/task.go b/pkg/web/handler/task.go index 08984bb..b68b11d 100644 --- a/pkg/web/handler/task.go +++ b/pkg/web/handler/task.go @@ -30,7 +30,11 @@ func HandleTaskStatus(w http.ResponseWriter, r *http.Request) { hx := HTMX(r) if hx.IsHxRequest() { - hx.WriteString(`` + taskFeedback.Message + ``) + hx.WriteString(` +
+ +
`) + return } @@ -39,6 +43,7 @@ func HandleTaskStatus(w http.ResponseWriter, r *http.Request) { } func HandleTaskStatusModal(w http.ResponseWriter, r *http.Request) { + time.Sleep(100 * time.Millisecond) err := components.Modal(components.ModalProps{ ID: "task-status-modal", Title: "Task Status", @@ -59,8 +64,8 @@ func feedbackList(feedback ValidationFeedback) templ.Component { } else { io.WriteString(w, `

`+"✅ "+check.Message+`

`) } - } + return nil }) }