From fe72bd4ddcb7e71ca30f936bd79fbafe9076bba1 Mon Sep 17 00:00:00 2001 From: Mike Lady Date: Sat, 3 Jan 2026 18:03:51 -0800 Subject: [PATCH] feat(dashboard): Add dynamic work status column for convoys MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The status column now shows computed work status based on progress and activity: - "complete" (green) - all tracked items are done - "active" (green) - recent polecat activity (within 1 min) - "stale" (yellow) - older activity (1-5 min) - "stuck" (red) - stale activity (5+ min) - "waiting" (gray) - no assignee/activity Previously the status column always showed "open" since we only fetch open convoys, making it static and uninformative. Changes: - templates.go: Add WorkStatus field to ConvoyRow, add workStatusClass func - fetcher.go: Add calculateWorkStatus() to compute status from progress/activity - convoy.html: Add work status badge styling, use WorkStatus in table 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- internal/web/fetcher.go | 63 +++++++++++++++++++++++++++--- internal/web/templates.go | 22 ++++++++++- internal/web/templates/convoy.html | 40 +++++++++++++++++-- 3 files changed, 116 insertions(+), 9 deletions(-) diff --git a/internal/web/fetcher.go b/internal/web/fetcher.go index 942e3816..c3c218a7 100644 --- a/internal/web/fetcher.go +++ b/internal/web/fetcher.go @@ -120,6 +120,9 @@ func (f *LiveConvoyFetcher) FetchConvoys() ([]ConvoyRow, error) { } } + // Calculate work status based on progress and activity + row.WorkStatus = calculateWorkStatus(row.Completed, row.Total, row.LastActivity.ColorClass) + // Get tracked issues for expandable view row.TrackedIssues = make([]TrackedIssue, len(tracked)) for i, t := range tracked { @@ -415,6 +418,27 @@ func (f *LiveConvoyFetcher) getAllPolecatActivity() *time.Time { return &mostRecent } +// calculateWorkStatus determines the work status based on progress and activity. +// Returns: "complete", "active", "stale", "stuck", or "waiting" +func calculateWorkStatus(completed, total int, activityColor string) string { + // Check if all work is done + if total > 0 && completed == total { + return "complete" + } + + // Determine status based on activity color + switch activityColor { + case activity.ColorGreen: + return "active" + case activity.ColorYellow: + return "stale" + case activity.ColorRed: + return "stuck" + default: + return "waiting" + } +} + // FetchMergeQueue fetches open PRs from configured repos. func (f *LiveConvoyFetcher) FetchMergeQueue() ([]MergeQueueRow, error) { // Repos to query for PRs @@ -568,7 +592,7 @@ func determineColorClass(ciStatus, mergeable string) string { return "mq-yellow" } -// FetchPolecats fetches all running polecat sessions with activity data. +// FetchPolecats fetches all running polecat and refinery sessions with activity data. func (f *LiveConvoyFetcher) FetchPolecats() ([]PolecatRow, error) { // Query all tmux sessions cmd := exec.Command("tmux", "list-sessions", "-F", "#{session_name}|#{session_activity}") @@ -579,6 +603,9 @@ func (f *LiveConvoyFetcher) FetchPolecats() ([]PolecatRow, error) { return nil, nil } + // Pre-fetch merge queue count to determine refinery idle status + mergeQueueCount := f.getMergeQueueCount() + var polecats []PolecatRow lines := strings.Split(strings.TrimSpace(stdout.String()), "\n") @@ -607,8 +634,9 @@ func (f *LiveConvoyFetcher) FetchPolecats() ([]PolecatRow, error) { rig := nameParts[1] polecat := nameParts[2] - // Skip non-polecat sessions (refinery, witness, mayor, deacon, boot) - if polecat == "refinery" || polecat == "witness" || polecat == "mayor" || polecat == "deacon" || polecat == "boot" { + // Skip non-worker sessions (witness, mayor, deacon, boot) + // Note: refinery is included to show idle/processing status + if polecat == "witness" || polecat == "mayor" || polecat == "deacon" || polecat == "boot" { continue } @@ -619,8 +647,13 @@ func (f *LiveConvoyFetcher) FetchPolecats() ([]PolecatRow, error) { } activityTime := time.Unix(activityUnix, 0) - // Get status hint from last line of pane - statusHint := f.getPolecatStatusHint(sessionName) + // Get status hint - special handling for refinery + var statusHint string + if polecat == "refinery" { + statusHint = f.getRefineryStatusHint(mergeQueueCount) + } else { + statusHint = f.getPolecatStatusHint(sessionName) + } polecats = append(polecats, PolecatRow{ Name: polecat, @@ -657,3 +690,23 @@ func (f *LiveConvoyFetcher) getPolecatStatusHint(sessionName string) string { } return "" } + +// getMergeQueueCount returns the total number of open PRs across all repos. +func (f *LiveConvoyFetcher) getMergeQueueCount() int { + mergeQueue, err := f.FetchMergeQueue() + if err != nil { + return 0 + } + return len(mergeQueue) +} + +// getRefineryStatusHint returns appropriate status for refinery based on merge queue. +func (f *LiveConvoyFetcher) getRefineryStatusHint(mergeQueueCount int) string { + if mergeQueueCount == 0 { + return "Idle - Waiting for PRs" + } + if mergeQueueCount == 1 { + return "Processing 1 PR" + } + return fmt.Sprintf("Processing %d PRs", mergeQueueCount) +} diff --git a/internal/web/templates.go b/internal/web/templates.go index 00fb2532..261cd8d8 100644 --- a/internal/web/templates.go +++ b/internal/web/templates.go @@ -43,7 +43,8 @@ type MergeQueueRow struct { type ConvoyRow struct { ID string Title string - Status string // "open" or "closed" + Status string // "open" or "closed" (raw beads status) + WorkStatus string // Computed: "complete", "active", "stale", "stuck", "waiting" Progress string // e.g., "2/5" Completed int Total int @@ -65,6 +66,7 @@ func LoadTemplates() (*template.Template, error) { funcMap := template.FuncMap{ "activityClass": activityClass, "statusClass": statusClass, + "workStatusClass": workStatusClass, "progressPercent": progressPercent, } @@ -109,6 +111,24 @@ func statusClass(status string) string { } } +// workStatusClass returns the CSS class for a computed work status. +func workStatusClass(workStatus string) string { + switch workStatus { + case "complete": + return "work-complete" + case "active": + return "work-active" + case "stale": + return "work-stale" + case "stuck": + return "work-stuck" + case "waiting": + return "work-waiting" + default: + return "work-unknown" + } +} + // progressPercent calculates percentage as an integer for progress bars. func progressPercent(completed, total int) int { if total == 0 { diff --git a/internal/web/templates/convoy.html b/internal/web/templates/convoy.html index b52fc1a9..65901019 100644 --- a/internal/web/templates/convoy.html +++ b/internal/web/templates/convoy.html @@ -104,6 +104,41 @@ background: var(--green); } + /* Work status badges */ + .work-status { + display: inline-block; + padding: 2px 8px; + border-radius: 4px; + font-size: 0.75rem; + font-weight: 500; + text-transform: uppercase; + } + + .work-complete .work-status { + background: var(--green); + color: var(--bg-dark); + } + + .work-active .work-status { + background: var(--green); + color: var(--bg-dark); + } + + .work-stale .work-status { + background: var(--yellow); + color: var(--bg-dark); + } + + .work-stuck .work-status { + background: var(--red); + color: var(--bg-dark); + } + + .work-waiting .work-status { + background: var(--text-secondary); + color: var(--bg-dark); + } + /* Activity colors */ .activity-dot { display: inline-block; @@ -296,10 +331,9 @@ {{range .Convoys}} - + - - {{if eq .Status "open"}}●{{else}}✓{{end}} + {{.WorkStatus}} {{.ID}}