feat(dashboard): Add dynamic work status column for convoys
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .Convoys}}
|
||||
<tr class="{{statusClass .Status}}">
|
||||
<tr class="{{workStatusClass .WorkStatus}}">
|
||||
<td>
|
||||
<span class="status-indicator"></span>
|
||||
{{if eq .Status "open"}}●{{else}}✓{{end}}
|
||||
<span class="work-status">{{.WorkStatus}}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="convoy-id">{{.ID}}</span>
|
||||
|
||||
Reference in New Issue
Block a user