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>
This commit is contained in:
181
internal/web/handler_test.go
Normal file
181
internal/web/handler_test.go
Normal file
@@ -0,0 +1,181 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/steveyegge/gastown/internal/activity"
|
||||
)
|
||||
|
||||
// MockConvoyFetcher is a mock implementation for testing.
|
||||
type MockConvoyFetcher struct {
|
||||
Convoys []ConvoyRow
|
||||
Error error
|
||||
}
|
||||
|
||||
func (m *MockConvoyFetcher) FetchConvoys() ([]ConvoyRow, error) {
|
||||
return m.Convoys, m.Error
|
||||
}
|
||||
|
||||
func TestConvoyHandler_RendersTemplate(t *testing.T) {
|
||||
mock := &MockConvoyFetcher{
|
||||
Convoys: []ConvoyRow{
|
||||
{
|
||||
ID: "hq-cv-abc",
|
||||
Title: "Test Convoy",
|
||||
Status: "open",
|
||||
Progress: "2/5",
|
||||
Completed: 2,
|
||||
Total: 5,
|
||||
LastActivity: activity.Calculate(time.Now().Add(-1 * time.Minute)),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
handler, err := NewConvoyHandler(mock)
|
||||
if err != nil {
|
||||
t.Fatalf("NewConvoyHandler() error = %v", err)
|
||||
}
|
||||
|
||||
req := httptest.NewRequest("GET", "/", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("Status = %d, want %d", w.Code, http.StatusOK)
|
||||
}
|
||||
|
||||
body := w.Body.String()
|
||||
|
||||
// Check convoy data is rendered
|
||||
if !strings.Contains(body, "hq-cv-abc") {
|
||||
t.Error("Response should contain convoy ID")
|
||||
}
|
||||
if !strings.Contains(body, "Test Convoy") {
|
||||
t.Error("Response should contain convoy title")
|
||||
}
|
||||
if !strings.Contains(body, "2/5") {
|
||||
t.Error("Response should contain progress")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvoyHandler_LastActivityColors(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
age time.Duration
|
||||
wantClass string
|
||||
}{
|
||||
{"green for active", 30 * time.Second, "activity-green"},
|
||||
{"yellow for stale", 3 * time.Minute, "activity-yellow"},
|
||||
{"red for stuck", 10 * time.Minute, "activity-red"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
mock := &MockConvoyFetcher{
|
||||
Convoys: []ConvoyRow{
|
||||
{
|
||||
ID: "hq-cv-test",
|
||||
Title: "Test",
|
||||
Status: "open",
|
||||
LastActivity: activity.Calculate(time.Now().Add(-tt.age)),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
handler, err := NewConvoyHandler(mock)
|
||||
if err != nil {
|
||||
t.Fatalf("NewConvoyHandler() error = %v", err)
|
||||
}
|
||||
|
||||
req := httptest.NewRequest("GET", "/", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.ServeHTTP(w, req)
|
||||
|
||||
body := w.Body.String()
|
||||
if !strings.Contains(body, tt.wantClass) {
|
||||
t.Errorf("Response should contain %q", tt.wantClass)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvoyHandler_EmptyConvoys(t *testing.T) {
|
||||
mock := &MockConvoyFetcher{
|
||||
Convoys: []ConvoyRow{},
|
||||
}
|
||||
|
||||
handler, err := NewConvoyHandler(mock)
|
||||
if err != nil {
|
||||
t.Fatalf("NewConvoyHandler() error = %v", err)
|
||||
}
|
||||
|
||||
req := httptest.NewRequest("GET", "/", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("Status = %d, want %d", w.Code, http.StatusOK)
|
||||
}
|
||||
|
||||
body := w.Body.String()
|
||||
if !strings.Contains(body, "No convoys") {
|
||||
t.Error("Response should show empty state message")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvoyHandler_ContentType(t *testing.T) {
|
||||
mock := &MockConvoyFetcher{
|
||||
Convoys: []ConvoyRow{},
|
||||
}
|
||||
|
||||
handler, err := NewConvoyHandler(mock)
|
||||
if err != nil {
|
||||
t.Fatalf("NewConvoyHandler() error = %v", err)
|
||||
}
|
||||
|
||||
req := httptest.NewRequest("GET", "/", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.ServeHTTP(w, req)
|
||||
|
||||
contentType := w.Header().Get("Content-Type")
|
||||
if !strings.Contains(contentType, "text/html") {
|
||||
t.Errorf("Content-Type = %q, want text/html", contentType)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvoyHandler_MultipleConvoys(t *testing.T) {
|
||||
mock := &MockConvoyFetcher{
|
||||
Convoys: []ConvoyRow{
|
||||
{ID: "hq-cv-1", Title: "First Convoy", Status: "open"},
|
||||
{ID: "hq-cv-2", Title: "Second Convoy", Status: "closed"},
|
||||
{ID: "hq-cv-3", Title: "Third Convoy", Status: "open"},
|
||||
},
|
||||
}
|
||||
|
||||
handler, err := NewConvoyHandler(mock)
|
||||
if err != nil {
|
||||
t.Fatalf("NewConvoyHandler() error = %v", err)
|
||||
}
|
||||
|
||||
req := httptest.NewRequest("GET", "/", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.ServeHTTP(w, req)
|
||||
|
||||
body := w.Body.String()
|
||||
|
||||
// Check all convoys are rendered
|
||||
for _, id := range []string{"hq-cv-1", "hq-cv-2", "hq-cv-3"} {
|
||||
if !strings.Contains(body, id) {
|
||||
t.Errorf("Response should contain convoy %s", id)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user