package views import ( "encoding/json" "fmt" "strings" ops "fn-registry/fn_operations" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" "github.com/lucasdataproyects/devfactory/tui" ) type historyState int const ( historyLoading historyState = iota historyList historyDetail ) type historyLoadedMsg []ops.Execution // HistoryModel shows execution history. type HistoryModel struct { state historyState list tui.FilteredListModel spinner tui.SpinnerModel styles tui.Styles executions []ops.Execution detail string scrollOff int opsDB *ops.DB pipelineNames map[string]string } // NewHistoryModel creates a new history view. func NewHistoryModel(styles tui.Styles, opsDB *ops.DB, names map[string]string) HistoryModel { return HistoryModel{ state: historyLoading, list: tui.NewFilteredList(nil, "Filter executions..."), spinner: tui.NewSpinner("Loading history..."), styles: styles, opsDB: opsDB, pipelineNames: names, } } func (m HistoryModel) Init() tea.Cmd { return tea.Batch( m.spinner.Init(), m.loadHistory(), ) } func (m HistoryModel) loadHistory() tea.Cmd { return func() tea.Msg { execs, err := m.opsDB.ListExecutions("", "", "") if err != nil { return historyLoadedMsg(nil) } return historyLoadedMsg(execs) } } func (m HistoryModel) Update(msg tea.Msg) (HistoryModel, tea.Cmd) { switch msg := msg.(type) { case historyLoadedMsg: m.executions = []ops.Execution(msg) items := make([]tui.ListItem, len(m.executions)) for i, e := range m.executions { icon := "●" switch e.Status { case ops.ExecSuccess: icon = "✓" case ops.ExecFailure: icon = "✗" case ops.ExecPartial: icon = "~" } name := e.PipelineID if n, ok := m.pipelineNames[e.PipelineID]; ok { name = n } dur := "" if e.DurationMs != nil { dur = fmt.Sprintf("%dms", *e.DurationMs) } items[i] = tui.ListItem{ Title: fmt.Sprintf("%s %s", icon, name), Description: fmt.Sprintf("%s — %s — %s", string(e.Status), dur, e.StartedAt.Format("2006-01-02 15:04:05")), Value: e, } } m.list.SetItems(items) m.state = historyList return m, nil case tea.KeyMsg: switch m.state { case historyList: switch msg.String() { case "r": m.state = historyLoading m.spinner = tui.NewSpinner("Loading history...") return m, tea.Batch(m.spinner.Init(), m.loadHistory()) case "enter": if item := m.list.SelectedItem(); item != nil { e := item.Value.(ops.Execution) m.detail = formatExecution(e) m.state = historyDetail m.scrollOff = 0 } return m, nil } case historyDetail: switch msg.String() { case "j", "down": m.scrollOff++ case "k", "up": if m.scrollOff > 0 { m.scrollOff-- } } return m, nil } } // Delegate to sub-components var cmd tea.Cmd switch m.state { case historyLoading: var spinnerModel tea.Model spinnerModel, cmd = m.spinner.Update(msg) m.spinner = spinnerModel.(tui.SpinnerModel) case historyList: var listModel tea.Model listModel, cmd = m.list.Update(msg) m.list = listModel.(tui.FilteredListModel) } return m, cmd } // HandleBack retrocede un nivel. Retorna true si ya en estado base. func (m *HistoryModel) HandleBack() bool { switch m.state { case historyDetail: m.state = historyList return false default: return true } } func (m HistoryModel) View() string { switch m.state { case historyLoading: return m.spinner.View() case historyList: if len(m.executions) == 0 { return m.styles.Muted.Render("No executions found. Launch a pipeline first.") } help := m.styles.Muted.Render(" Enter: details │ r: refresh │ /: filter") return m.list.View() + "\n" + help case historyDetail: return m.renderDetail() } return "" } func (m HistoryModel) renderDetail() string { lines := splitLines(m.detail) maxLines := 20 if m.scrollOff >= len(lines) { m.scrollOff = max(0, len(lines)-1) } end := min(m.scrollOff+maxLines, len(lines)) visible := lines[m.scrollOff:end] header := m.styles.Header.Render("Execution Detail") content := lipgloss.JoinVertical(lipgloss.Left, visible...) help := m.styles.Muted.Render(" j/k: scroll │ Esc: back") return header + "\n" + content + "\n" + help } func formatExecution(e ops.Execution) string { var sb strings.Builder sb.WriteString(fmt.Sprintf("ID: %s\n", e.ID)) sb.WriteString(fmt.Sprintf("Pipeline: %s\n", e.PipelineID)) sb.WriteString(fmt.Sprintf("Status: %s\n", e.Status)) sb.WriteString(fmt.Sprintf("Started: %s\n", e.StartedAt.Format("2006-01-02 15:04:05"))) if e.EndedAt != nil { sb.WriteString(fmt.Sprintf("Ended: %s\n", e.EndedAt.Format("2006-01-02 15:04:05"))) } if e.DurationMs != nil { sb.WriteString(fmt.Sprintf("Duration: %dms\n", *e.DurationMs)) } if e.RecordsIn != nil { sb.WriteString(fmt.Sprintf("Records In: %d\n", *e.RecordsIn)) } if e.RecordsOut != nil { sb.WriteString(fmt.Sprintf("Records Out: %d\n", *e.RecordsOut)) } if e.Error != "" { sb.WriteString(fmt.Sprintf("\n--- Error ---\n%s\n", e.Error)) } if len(e.Metrics) > 0 { sb.WriteString("\n--- Metrics ---\n") b, _ := json.MarshalIndent(e.Metrics, "", " ") sb.WriteString(string(b)) sb.WriteString("\n") } return sb.String() }