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
|
// Get tracked issues for expandable view
|
||||||
row.TrackedIssues = make([]TrackedIssue, len(tracked))
|
row.TrackedIssues = make([]TrackedIssue, len(tracked))
|
||||||
for i, t := range tracked {
|
for i, t := range tracked {
|
||||||
@@ -415,6 +418,27 @@ func (f *LiveConvoyFetcher) getAllPolecatActivity() *time.Time {
|
|||||||
return &mostRecent
|
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.
|
// FetchMergeQueue fetches open PRs from configured repos.
|
||||||
func (f *LiveConvoyFetcher) FetchMergeQueue() ([]MergeQueueRow, error) {
|
func (f *LiveConvoyFetcher) FetchMergeQueue() ([]MergeQueueRow, error) {
|
||||||
// Repos to query for PRs
|
// Repos to query for PRs
|
||||||
@@ -568,7 +592,7 @@ func determineColorClass(ciStatus, mergeable string) string {
|
|||||||
return "mq-yellow"
|
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) {
|
func (f *LiveConvoyFetcher) FetchPolecats() ([]PolecatRow, error) {
|
||||||
// Query all tmux sessions
|
// Query all tmux sessions
|
||||||
cmd := exec.Command("tmux", "list-sessions", "-F", "#{session_name}|#{session_activity}")
|
cmd := exec.Command("tmux", "list-sessions", "-F", "#{session_name}|#{session_activity}")
|
||||||
@@ -579,6 +603,9 @@ func (f *LiveConvoyFetcher) FetchPolecats() ([]PolecatRow, error) {
|
|||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Pre-fetch merge queue count to determine refinery idle status
|
||||||
|
mergeQueueCount := f.getMergeQueueCount()
|
||||||
|
|
||||||
var polecats []PolecatRow
|
var polecats []PolecatRow
|
||||||
lines := strings.Split(strings.TrimSpace(stdout.String()), "\n")
|
lines := strings.Split(strings.TrimSpace(stdout.String()), "\n")
|
||||||
|
|
||||||
@@ -607,8 +634,9 @@ func (f *LiveConvoyFetcher) FetchPolecats() ([]PolecatRow, error) {
|
|||||||
rig := nameParts[1]
|
rig := nameParts[1]
|
||||||
polecat := nameParts[2]
|
polecat := nameParts[2]
|
||||||
|
|
||||||
// Skip non-polecat sessions (refinery, witness, mayor, deacon, boot)
|
// Skip non-worker sessions (witness, mayor, deacon, boot)
|
||||||
if polecat == "refinery" || polecat == "witness" || polecat == "mayor" || polecat == "deacon" || polecat == "boot" {
|
// Note: refinery is included to show idle/processing status
|
||||||
|
if polecat == "witness" || polecat == "mayor" || polecat == "deacon" || polecat == "boot" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -619,8 +647,13 @@ func (f *LiveConvoyFetcher) FetchPolecats() ([]PolecatRow, error) {
|
|||||||
}
|
}
|
||||||
activityTime := time.Unix(activityUnix, 0)
|
activityTime := time.Unix(activityUnix, 0)
|
||||||
|
|
||||||
// Get status hint from last line of pane
|
// Get status hint - special handling for refinery
|
||||||
statusHint := f.getPolecatStatusHint(sessionName)
|
var statusHint string
|
||||||
|
if polecat == "refinery" {
|
||||||
|
statusHint = f.getRefineryStatusHint(mergeQueueCount)
|
||||||
|
} else {
|
||||||
|
statusHint = f.getPolecatStatusHint(sessionName)
|
||||||
|
}
|
||||||
|
|
||||||
polecats = append(polecats, PolecatRow{
|
polecats = append(polecats, PolecatRow{
|
||||||
Name: polecat,
|
Name: polecat,
|
||||||
@@ -657,3 +690,23 @@ func (f *LiveConvoyFetcher) getPolecatStatusHint(sessionName string) string {
|
|||||||
}
|
}
|
||||||
return ""
|
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 {
|
type ConvoyRow struct {
|
||||||
ID string
|
ID string
|
||||||
Title 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"
|
Progress string // e.g., "2/5"
|
||||||
Completed int
|
Completed int
|
||||||
Total int
|
Total int
|
||||||
@@ -65,6 +66,7 @@ func LoadTemplates() (*template.Template, error) {
|
|||||||
funcMap := template.FuncMap{
|
funcMap := template.FuncMap{
|
||||||
"activityClass": activityClass,
|
"activityClass": activityClass,
|
||||||
"statusClass": statusClass,
|
"statusClass": statusClass,
|
||||||
|
"workStatusClass": workStatusClass,
|
||||||
"progressPercent": progressPercent,
|
"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.
|
// progressPercent calculates percentage as an integer for progress bars.
|
||||||
func progressPercent(completed, total int) int {
|
func progressPercent(completed, total int) int {
|
||||||
if total == 0 {
|
if total == 0 {
|
||||||
|
|||||||
@@ -104,6 +104,41 @@
|
|||||||
background: var(--green);
|
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 colors */
|
||||||
.activity-dot {
|
.activity-dot {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
@@ -296,10 +331,9 @@
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{{range .Convoys}}
|
{{range .Convoys}}
|
||||||
<tr class="{{statusClass .Status}}">
|
<tr class="{{workStatusClass .WorkStatus}}">
|
||||||
<td>
|
<td>
|
||||||
<span class="status-indicator"></span>
|
<span class="work-status">{{.WorkStatus}}</span>
|
||||||
{{if eq .Status "open"}}●{{else}}✓{{end}}
|
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<span class="convoy-id">{{.ID}}</span>
|
<span class="convoy-id">{{.ID}}</span>
|
||||||
|
|||||||
Reference in New Issue
Block a user