Files
gastown/internal/activity/activity.go
Mike Lady 3488933cc2 Add 'gt dashboard' CLI command (hq-s1bg) (#65)
* Add LastActivity calculation for convoy dashboard (hq-x2xy)

Adds internal/activity package with color-coded activity tracking:
- Green: <2 minutes (active)
- Yellow: 2-5 minutes (stale)
- Red: >5 minutes (stuck)

Features:
- Calculate() function returns Info with formatted age and color class
- Helper methods: IsActive(), IsStale(), IsStuck()
- Handles edge cases: zero time, future time (clock skew)

Tests: 8 test functions with 25 sub-tests covering all thresholds.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Add convoy dashboard HTML template with Last Activity (hq-fq1g)

Adds internal/web package with convoy dashboard template:
- convoy.html with Last Activity column and color coding
- Green (<2min), Yellow (2-5min), Red (>5min) activity indicators
- htmx auto-refresh every 30 seconds
- Progress bars for convoy completion
- Status indicators for open/closed convoys
- Empty state when no convoys

Also includes internal/activity package (dependency from hq-x2xy):
- Calculate() returns Info with formatted age and color class
- Helper methods: IsActive(), IsStale(), IsStuck()

Tests: 6 template tests + 8 activity tests, all passing.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Add convoy list handler with activity data (hq-3edt)

Adds HTTP handler that wires convoy dashboard template to real data:
- ConvoyHandler: HTTP handler for GET / rendering convoy dashboard
- LiveConvoyFetcher: Fetches convoys from beads with activity data
- ConvoyFetcher interface: Enables mocking for tests

Features:
- Fetches open convoys from town beads
- Calculates progress (completed/total) from tracked issues
- Gets Last Activity from worker agent beads
- Color codes activity: Green (<2min), Yellow (2-5min), Red (>5min)

Includes dependencies (not yet merged):
- internal/activity: Activity calculation (hq-x2xy)
- internal/web/templates: HTML template (hq-fq1g)

Tests: 5 handler tests + 6 template tests + 8 activity tests = 19 total

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Add 'gt dashboard' CLI command (hq-s1bg)

Add dashboard command to start the convoy tracking web server.

Usage: gt dashboard [--port=8080] [--open]

Features:
- --port: Configurable HTTP port (default 8080)
- --open: Auto-open browser on start
- Cross-platform browser launch (darwin/linux/windows)
- Graceful workspace detection

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-03 11:49:42 -08:00

134 lines
3.2 KiB
Go

// Package activity provides last-activity tracking and color-coding for the dashboard.
package activity
import (
"time"
)
// Color class constants for activity status.
const (
ColorGreen = "green" // Active: <2 minutes
ColorYellow = "yellow" // Stale: 2-5 minutes
ColorRed = "red" // Stuck: >5 minutes
ColorUnknown = "unknown" // No activity data
)
// Thresholds for activity color coding.
const (
ThresholdActive = 2 * time.Minute // Green threshold
ThresholdStale = 5 * time.Minute // Yellow threshold (beyond this is red)
)
// Info holds activity information for display.
type Info struct {
LastActivity time.Time // Raw timestamp of last activity
Duration time.Duration // Time since last activity
FormattedAge string // Human-readable age (e.g., "2m", "1h")
ColorClass string // CSS class for coloring (green, yellow, red, unknown)
}
// Calculate computes activity info from a last-activity timestamp.
// Returns color-coded info based on thresholds:
// - Green: <2 minutes (active)
// - Yellow: 2-5 minutes (stale)
// - Red: >5 minutes (stuck)
// - Unknown: zero time value
func Calculate(lastActivity time.Time) Info {
info := Info{
LastActivity: lastActivity,
}
// Handle zero time (no activity data)
if lastActivity.IsZero() {
info.FormattedAge = "unknown"
info.ColorClass = ColorUnknown
return info
}
// Calculate duration since last activity
info.Duration = time.Since(lastActivity)
// Handle future time (clock skew) - treat as just now
if info.Duration < 0 {
info.Duration = 0
}
// Format age string
info.FormattedAge = formatAge(info.Duration)
// Determine color class
info.ColorClass = colorForDuration(info.Duration)
return info
}
// formatAge formats a duration as a short human-readable string.
// Examples: "<1m", "5m", "2h", "1d"
func formatAge(d time.Duration) string {
if d < time.Minute {
return "<1m"
}
if d < time.Hour {
return formatMinutes(d)
}
if d < 24*time.Hour {
return formatHours(d)
}
return formatDays(d)
}
func formatMinutes(d time.Duration) string {
mins := int(d.Minutes())
return formatInt(mins) + "m"
}
func formatHours(d time.Duration) string {
hours := int(d.Hours())
return formatInt(hours) + "h"
}
func formatDays(d time.Duration) string {
days := int(d.Hours() / 24)
return formatInt(days) + "d"
}
func formatInt(n int) string {
if n < 10 {
return string(rune('0'+n))
}
// For larger numbers, use standard conversion
result := ""
for n > 0 {
result = string(rune('0'+n%10)) + result
n /= 10
}
return result
}
// colorForDuration returns the color class for a given duration.
func colorForDuration(d time.Duration) string {
switch {
case d < ThresholdActive:
return ColorGreen
case d < ThresholdStale:
return ColorYellow
default:
return ColorRed
}
}
// IsActive returns true if the activity is within the active threshold (green).
func (i Info) IsActive() bool {
return i.ColorClass == ColorGreen
}
// IsStale returns true if the activity is in the stale range (yellow).
func (i Info) IsStale() bool {
return i.ColorClass == ColorYellow
}
// IsStuck returns true if the activity is beyond the stale threshold (red).
func (i Info) IsStuck() bool {
return i.ColorClass == ColorRed
}