Files
gastown/internal/web/templates_test.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

239 lines
5.1 KiB
Go

package web
import (
"bytes"
"strings"
"testing"
"time"
"github.com/steveyegge/gastown/internal/activity"
)
func TestConvoyTemplate_RendersConvoyList(t *testing.T) {
tmpl, err := LoadTemplates()
if err != nil {
t.Fatalf("LoadTemplates() error = %v", err)
}
data := ConvoyData{
Convoys: []ConvoyRow{
{
ID: "hq-cv-abc",
Title: "Feature X",
Status: "open",
Progress: "2/5",
Completed: 2,
Total: 5,
LastActivity: activity.Calculate(time.Now().Add(-1 * time.Minute)),
},
{
ID: "hq-cv-def",
Title: "Bugfix Y",
Status: "open",
Progress: "1/3",
Completed: 1,
Total: 3,
LastActivity: activity.Calculate(time.Now().Add(-3 * time.Minute)),
},
},
}
var buf bytes.Buffer
err = tmpl.ExecuteTemplate(&buf, "convoy.html", data)
if err != nil {
t.Fatalf("ExecuteTemplate() error = %v", err)
}
output := buf.String()
// Check convoy IDs are rendered
if !strings.Contains(output, "hq-cv-abc") {
t.Error("Template should contain convoy ID hq-cv-abc")
}
if !strings.Contains(output, "hq-cv-def") {
t.Error("Template should contain convoy ID hq-cv-def")
}
// Check titles are rendered
if !strings.Contains(output, "Feature X") {
t.Error("Template should contain title 'Feature X'")
}
if !strings.Contains(output, "Bugfix Y") {
t.Error("Template should contain title 'Bugfix Y'")
}
}
func TestConvoyTemplate_LastActivityColors(t *testing.T) {
tmpl, err := LoadTemplates()
if err != nil {
t.Fatalf("LoadTemplates() error = %v", err)
}
tests := []struct {
name string
age time.Duration
wantClass string
}{
{"green for 1 minute", 1 * time.Minute, "activity-green"},
{"yellow for 3 minutes", 3 * time.Minute, "activity-yellow"},
{"red for 10 minutes", 10 * time.Minute, "activity-red"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
data := ConvoyData{
Convoys: []ConvoyRow{
{
ID: "hq-cv-test",
Title: "Test",
Status: "open",
LastActivity: activity.Calculate(time.Now().Add(-tt.age)),
},
},
}
var buf bytes.Buffer
err = tmpl.ExecuteTemplate(&buf, "convoy.html", data)
if err != nil {
t.Fatalf("ExecuteTemplate() error = %v", err)
}
output := buf.String()
if !strings.Contains(output, tt.wantClass) {
t.Errorf("Template should contain class %q for %v age", tt.wantClass, tt.age)
}
})
}
}
func TestConvoyTemplate_HtmxAutoRefresh(t *testing.T) {
tmpl, err := LoadTemplates()
if err != nil {
t.Fatalf("LoadTemplates() error = %v", err)
}
data := ConvoyData{
Convoys: []ConvoyRow{
{
ID: "hq-cv-test",
Title: "Test",
Status: "open",
},
},
}
var buf bytes.Buffer
err = tmpl.ExecuteTemplate(&buf, "convoy.html", data)
if err != nil {
t.Fatalf("ExecuteTemplate() error = %v", err)
}
output := buf.String()
// Check for htmx attributes
if !strings.Contains(output, "hx-get") {
t.Error("Template should contain hx-get for auto-refresh")
}
if !strings.Contains(output, "hx-trigger") {
t.Error("Template should contain hx-trigger for auto-refresh")
}
if !strings.Contains(output, "every 30s") {
t.Error("Template should refresh every 30 seconds")
}
}
func TestConvoyTemplate_ProgressDisplay(t *testing.T) {
tmpl, err := LoadTemplates()
if err != nil {
t.Fatalf("LoadTemplates() error = %v", err)
}
data := ConvoyData{
Convoys: []ConvoyRow{
{
ID: "hq-cv-test",
Title: "Test",
Status: "open",
Progress: "3/7",
Completed: 3,
Total: 7,
},
},
}
var buf bytes.Buffer
err = tmpl.ExecuteTemplate(&buf, "convoy.html", data)
if err != nil {
t.Fatalf("ExecuteTemplate() error = %v", err)
}
output := buf.String()
// Check progress is displayed
if !strings.Contains(output, "3/7") {
t.Error("Template should display progress '3/7'")
}
}
func TestConvoyTemplate_StatusIndicators(t *testing.T) {
tmpl, err := LoadTemplates()
if err != nil {
t.Fatalf("LoadTemplates() error = %v", err)
}
data := ConvoyData{
Convoys: []ConvoyRow{
{
ID: "hq-cv-open",
Title: "Open Convoy",
Status: "open",
},
{
ID: "hq-cv-closed",
Title: "Closed Convoy",
Status: "closed",
},
},
}
var buf bytes.Buffer
err = tmpl.ExecuteTemplate(&buf, "convoy.html", data)
if err != nil {
t.Fatalf("ExecuteTemplate() error = %v", err)
}
output := buf.String()
// Check status indicators
if !strings.Contains(output, "status-open") {
t.Error("Template should contain status-open class")
}
if !strings.Contains(output, "status-closed") {
t.Error("Template should contain status-closed class")
}
}
func TestConvoyTemplate_EmptyState(t *testing.T) {
tmpl, err := LoadTemplates()
if err != nil {
t.Fatalf("LoadTemplates() error = %v", err)
}
data := ConvoyData{
Convoys: []ConvoyRow{},
}
var buf bytes.Buffer
err = tmpl.ExecuteTemplate(&buf, "convoy.html", data)
if err != nil {
t.Fatalf("ExecuteTemplate() error = %v", err)
}
output := buf.String()
// Check for empty state message
if !strings.Contains(output, "No convoys") {
t.Error("Template should show empty state message when no convoys")
}
}