Project>project overview from the dashboard endpoint; tools chooser drawn in Board>board; docs for Shell and t
This commit is contained in:
@@ -19,6 +19,7 @@ import (
|
||||
)
|
||||
|
||||
type projectMsg struct {
|
||||
dash paperclip.Dashboard
|
||||
project paperclip.Project
|
||||
cfg map[string]string
|
||||
agents []paperclip.Agent
|
||||
@@ -38,6 +39,7 @@ type projectModel struct {
|
||||
client *paperclip.Client
|
||||
folder string
|
||||
|
||||
dash paperclip.Dashboard
|
||||
project paperclip.Project
|
||||
cfg map[string]string
|
||||
agents []paperclip.Agent
|
||||
@@ -53,7 +55,7 @@ type projectModel struct {
|
||||
width, height int
|
||||
}
|
||||
|
||||
var projectTabs = []string{"facts", "costs", "settings"}
|
||||
var projectTabs = []string{"overview", "facts", "costs", "settings"}
|
||||
|
||||
func newProjectModel(c *paperclip.Client, folder string) projectModel {
|
||||
return projectModel{client: c, folder: folder}
|
||||
@@ -96,7 +98,8 @@ func (m projectModel) load() tea.Cmd {
|
||||
}
|
||||
costs = append(costs, taskCost{t.Identifier, t.Status, cost.CostCents, cost.RunCount})
|
||||
}
|
||||
return projectMsg{project: proj, cfg: cfg, agents: agents, costs: costs}
|
||||
dash, _ := c.DashboardSummary()
|
||||
return projectMsg{project: proj, cfg: cfg, agents: agents, costs: costs, dash: dash}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -182,6 +185,7 @@ func (m projectModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
}
|
||||
|
||||
case projectMsg:
|
||||
m.dash = msg.dash
|
||||
m.project = msg.project
|
||||
m.cfg = msg.cfg
|
||||
m.agents = msg.agents
|
||||
@@ -225,8 +229,10 @@ func (m projectModel) View() string {
|
||||
var body []string
|
||||
switch m.tab {
|
||||
case 0:
|
||||
body = m.factLines()
|
||||
body = m.overviewLines(m.width - 4)
|
||||
case 1:
|
||||
body = m.factLines()
|
||||
case 2:
|
||||
body = m.costLines()
|
||||
default:
|
||||
body = m.settingLines()
|
||||
@@ -268,6 +274,82 @@ func (m projectModel) row(label, value string) string {
|
||||
lipgloss.NewStyle().Foreground(lipgloss.Color(active.Text)).Render(value)
|
||||
}
|
||||
|
||||
// overviewLines is the company dashboard, drawn as numbers a person can act on
|
||||
// and a sparkline of the last two weeks of runs. Everything here comes from
|
||||
// GET /api/companies/{id}/dashboard in one call.
|
||||
func (m projectModel) overviewLines(width int) []string {
|
||||
d := m.dash
|
||||
out := []string{"", groupHeading("agents", -1, width), ""}
|
||||
out = append(out, " "+stat("active", d.Agents.Active, active.Text)+
|
||||
" "+stat("running", d.Agents.Running, active.Info)+
|
||||
" "+stat("paused", d.Agents.Paused, active.Warning)+
|
||||
" "+stat("error", d.Agents.Error, active.Error))
|
||||
out = append(out, "", groupHeading("tasks", -1, width), "")
|
||||
out = append(out, " "+stat("open", d.Tasks.Open, active.Text)+
|
||||
" "+stat("in progress", d.Tasks.InProgress, active.Info)+
|
||||
" "+stat("blocked", d.Tasks.Blocked, active.Error)+
|
||||
" "+stat("done", d.Tasks.Done, active.Success))
|
||||
out = append(out, "", groupHeading("money, this month", -1, width), "")
|
||||
budget := "no company budget set"
|
||||
if d.Costs.MonthBudgetCents > 0 {
|
||||
budget = fmt.Sprintf("%.0f%% of %d cents", d.Costs.MonthUtilizationPercent, d.Costs.MonthBudgetCents)
|
||||
}
|
||||
out = append(out, " "+stat("cents spent", d.Costs.MonthSpendCents, active.Text)+" "+
|
||||
mutedStyle2().Render(budget))
|
||||
out = append(out, " "+stat("pending approvals", d.PendingApprovals, active.Warning))
|
||||
if d.Budgets.ActiveIncidents > 0 || d.Budgets.PausedAgents > 0 || d.Budgets.PausedProjects > 0 {
|
||||
out = append(out, " "+stat("budget incidents", d.Budgets.ActiveIncidents, active.Error)+
|
||||
mutedStyle2().Render(fmt.Sprintf(" paused: %d agent(s), %d project(s)",
|
||||
d.Budgets.PausedAgents, d.Budgets.PausedProjects)))
|
||||
}
|
||||
out = append(out, "", groupHeading("runs, last two weeks", -1, width), "")
|
||||
out = append(out, " "+sparkline(d.RunActivity))
|
||||
if n := len(d.RunActivity); n > 0 {
|
||||
first, last := d.RunActivity[0].Date, d.RunActivity[n-1].Date
|
||||
out = append(out, dimStyle().Render(" "+first+" → "+last+" one bar per day, taller is more runs"))
|
||||
}
|
||||
out = append(out, "")
|
||||
out = append(out, dimStyle().Render(" read from Paperclip's own dashboard, not counted here."))
|
||||
return out
|
||||
}
|
||||
|
||||
// stat is a label and a number, the number first because it is what you look at.
|
||||
func stat(label string, value int, colour string) string {
|
||||
return lipgloss.NewStyle().Foreground(lipgloss.Color(colour)).Bold(true).Render(fmt.Sprintf("%d", value)) +
|
||||
" " + mutedStyle2().Render(label)
|
||||
}
|
||||
|
||||
// sparkline draws one bar per day using block characters, scaled so the busiest
|
||||
// day fills the height. No chart library, no second language: the data is in
|
||||
// memory and the terminal already knows how to draw blocks.
|
||||
func sparkline(points []struct {
|
||||
Date string `json:"date"`
|
||||
Total int `json:"total"`
|
||||
Succeeded int `json:"succeeded"`
|
||||
Failed int `json:"failed"`
|
||||
}) string {
|
||||
bars := []rune("▁▂▃▄▅▆▇█")
|
||||
peak := 0
|
||||
for _, p := range points {
|
||||
if p.Total > peak {
|
||||
peak = p.Total
|
||||
}
|
||||
}
|
||||
if peak == 0 {
|
||||
return mutedStyle2().Render("no runs in the last two weeks")
|
||||
}
|
||||
var b strings.Builder
|
||||
for _, p := range points {
|
||||
idx := int(float64(p.Total) / float64(peak) * float64(len(bars)-1))
|
||||
colour := active.Success
|
||||
if p.Failed > 0 {
|
||||
colour = active.Warning
|
||||
}
|
||||
b.WriteString(lipgloss.NewStyle().Foreground(lipgloss.Color(colour)).Render(string(bars[idx])))
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (m projectModel) factLines() []string {
|
||||
out := []string{""}
|
||||
if m.project.Name != "" {
|
||||
|
||||
Reference in New Issue
Block a user