* feat(dashboard): comprehensive control panel with expand/collapse
- Add 13 panels: Convoys, Polecats, Sessions, Activity, Mail, Merge Queue,
Escalations, Rigs, Dogs, System Health, Open Issues, Hooks, Queues
- Add Mayor status banner and Summary/Alerts section
- Implement instant client-side expand/collapse (no page reload)
- Add responsive grid layout for different window sizes
- Parallel data fetching for faster load times
- Color-coded mail by sender, chronological ordering
- Full titles visible in expanded views (no truncation)
- Auto-refresh every 10 seconds via HTMX
* fix(web): update tests and lint for dashboard control panel
- Update MockConvoyFetcher with 11 new interface methods
- Update MockConvoyFetcherWithErrors with matching methods
- Update test assertions for new template structure:
- Section headers ("Gas Town Convoys" -> "Convoys")
- Work status badges (badge-green, badge-yellow, badge-red)
- CI/merge status display text
- Empty state messages ("No active convoys")
- Fix linting: explicit _, _ = for fmt.Sscanf returns
Tests and linting now pass with the new dashboard features.
* perf(web): add timeouts and error logging to dashboard
Performance and reliability improvements:
- Add 8-second overall fetch timeout to prevent stuck requests
- Add per-command timeouts: 5s for bd/sqlite3, 10s for gh, 2s for tmux
- Add helper functions runCmd() and runBdCmd() with context timeout
- Add error logging for all 14 fetch operations
- Handler now returns partial data if timeout occurs
This addresses slow loading and "stuck" dashboard issues by ensuring
commands cannot hang indefinitely.
375 lines
9.8 KiB
Go
375 lines
9.8 KiB
Go
// Package web provides HTTP server and templates for the Gas Town dashboard.
|
|
package web
|
|
|
|
import (
|
|
"embed"
|
|
"html/template"
|
|
"io/fs"
|
|
|
|
"github.com/steveyegge/gastown/internal/activity"
|
|
)
|
|
|
|
//go:embed templates/*.html
|
|
var templateFS embed.FS
|
|
|
|
// ConvoyData represents data passed to the convoy template.
|
|
type ConvoyData struct {
|
|
Convoys []ConvoyRow
|
|
MergeQueue []MergeQueueRow
|
|
Polecats []PolecatRow
|
|
Mail []MailRow
|
|
Rigs []RigRow
|
|
Dogs []DogRow
|
|
Escalations []EscalationRow
|
|
Health *HealthRow
|
|
Queues []QueueRow
|
|
Sessions []SessionRow
|
|
Hooks []HookRow
|
|
Mayor *MayorStatus
|
|
Issues []IssueRow
|
|
Activity []ActivityRow
|
|
Summary *DashboardSummary
|
|
Expand string // Panel to show fullscreen (from ?expand=name)
|
|
}
|
|
|
|
// RigRow represents a registered rig in the dashboard.
|
|
type RigRow struct {
|
|
Name string
|
|
GitURL string
|
|
PolecatCount int
|
|
CrewCount int
|
|
HasWitness bool
|
|
HasRefinery bool
|
|
}
|
|
|
|
// DogRow represents a Deacon helper worker.
|
|
type DogRow struct {
|
|
Name string // Dog name (e.g., "alpha")
|
|
State string // idle, working
|
|
Work string // Current work assignment
|
|
LastActive string // Formatted age (e.g., "5m ago")
|
|
RigCount int // Number of worktrees
|
|
}
|
|
|
|
// EscalationRow represents an escalation needing attention.
|
|
type EscalationRow struct {
|
|
ID string
|
|
Title string
|
|
Severity string // critical, high, medium, low
|
|
EscalatedBy string
|
|
Age string
|
|
Acked bool
|
|
}
|
|
|
|
// HealthRow represents system health status.
|
|
type HealthRow struct {
|
|
DeaconHeartbeat string // Age of heartbeat (e.g., "2m ago")
|
|
DeaconCycle int64
|
|
HealthyAgents int
|
|
UnhealthyAgents int
|
|
IsPaused bool
|
|
PauseReason string
|
|
HeartbeatFresh bool // true if < 5min old
|
|
}
|
|
|
|
// QueueRow represents a work queue.
|
|
type QueueRow struct {
|
|
Name string
|
|
Status string // active, paused, closed
|
|
Available int
|
|
Processing int
|
|
Completed int
|
|
Failed int
|
|
}
|
|
|
|
// SessionRow represents a tmux session.
|
|
type SessionRow struct {
|
|
Name string // Session name (e.g., "gt-gastown-witness")
|
|
Role string // witness, refinery, polecat, crew, deacon
|
|
Rig string // Rig name if applicable
|
|
Worker string // Worker name for polecats/crew
|
|
Activity string // Age since last activity
|
|
IsAlive bool // Whether Claude is running in session
|
|
}
|
|
|
|
// HookRow represents a hooked bead (work pinned to an agent).
|
|
type HookRow struct {
|
|
ID string // Bead ID (e.g., "gt-abc12")
|
|
Title string // Work item title
|
|
Assignee string // Agent address (e.g., "gastown/polecats/nux")
|
|
Agent string // Formatted agent name
|
|
Age string // Time since hooked
|
|
IsStale bool // True if hooked > 1 hour (potentially stuck)
|
|
}
|
|
|
|
// MayorStatus represents the Mayor's current state.
|
|
type MayorStatus struct {
|
|
IsAttached bool // True if gt-mayor tmux session exists
|
|
SessionName string // Tmux session name
|
|
LastActivity string // Age since last activity
|
|
IsActive bool // True if activity < 5 min (likely working)
|
|
Runtime string // Which runtime (claude, codex, etc.)
|
|
}
|
|
|
|
// IssueRow represents an open issue in the backlog.
|
|
type IssueRow struct {
|
|
ID string // Bead ID (e.g., "gt-abc12")
|
|
Title string // Issue title
|
|
Type string // issue, bug, feature, task
|
|
Priority int // 1=critical, 2=high, 3=medium, 4=low
|
|
Age string // Time since created
|
|
Labels string // Comma-separated labels
|
|
}
|
|
|
|
// ActivityRow represents an event in the activity feed.
|
|
type ActivityRow struct {
|
|
Time string // Formatted time (e.g., "2m ago")
|
|
Icon string // Emoji for event type
|
|
Type string // Event type (sling, done, mail, etc.)
|
|
Actor string // Who did it
|
|
Summary string // Human-readable description
|
|
}
|
|
|
|
// DashboardSummary provides at-a-glance stats and alerts.
|
|
type DashboardSummary struct {
|
|
// Stats
|
|
PolecatCount int
|
|
HookCount int
|
|
IssueCount int
|
|
ConvoyCount int
|
|
EscalationCount int
|
|
|
|
// Alerts (things needing attention)
|
|
StuckPolecats int // No activity > 5 min
|
|
StaleHooks int // Hooked > 1 hour
|
|
UnackedEscalations int
|
|
DeadSessions int // Sessions that died recently
|
|
HighPriorityIssues int // P1/P2 issues
|
|
|
|
// Computed
|
|
HasAlerts bool
|
|
}
|
|
|
|
// MailRow represents a mail message in the dashboard.
|
|
type MailRow struct {
|
|
ID string // Message ID (e.g., "hq-msg-abc123")
|
|
From string // Sender (e.g., "gastown/polecats/Toast")
|
|
FromRaw string // Raw sender address for color hashing
|
|
To string // Recipient (e.g., "mayor/")
|
|
Subject string // Message subject
|
|
Timestamp string // Formatted timestamp
|
|
Age string // Human-readable age (e.g., "5m ago")
|
|
Priority string // low, normal, high, urgent
|
|
Type string // task, notification, reply
|
|
Read bool // Whether message has been read
|
|
SortKey int64 // Unix timestamp for sorting
|
|
}
|
|
|
|
// PolecatRow represents a polecat worker in the dashboard.
|
|
type PolecatRow struct {
|
|
Name string // e.g., "dag", "nux"
|
|
Rig string // e.g., "roxas", "gastown"
|
|
SessionID string // e.g., "gt-roxas-dag"
|
|
LastActivity activity.Info // Colored activity display
|
|
StatusHint string // Last line from pane (optional)
|
|
IssueID string // Currently assigned issue ID (e.g., "hq-1234")
|
|
IssueTitle string // Issue title (truncated)
|
|
WorkStatus string // working, stale, stuck, idle
|
|
}
|
|
|
|
// MergeQueueRow represents a PR in the merge queue.
|
|
type MergeQueueRow struct {
|
|
Number int
|
|
Repo string // Short repo name (e.g., "roxas", "gastown")
|
|
Title string
|
|
URL string
|
|
CIStatus string // "pass", "fail", "pending"
|
|
Mergeable string // "ready", "conflict", "pending"
|
|
ColorClass string // "mq-green", "mq-yellow", "mq-red"
|
|
}
|
|
|
|
// ConvoyRow represents a single convoy in the dashboard.
|
|
type ConvoyRow struct {
|
|
ID string
|
|
Title string
|
|
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
|
|
LastActivity activity.Info
|
|
TrackedIssues []TrackedIssue
|
|
}
|
|
|
|
// TrackedIssue represents an issue tracked by a convoy.
|
|
type TrackedIssue struct {
|
|
ID string
|
|
Title string
|
|
Status string
|
|
Assignee string
|
|
}
|
|
|
|
// LoadTemplates loads and parses all HTML templates.
|
|
func LoadTemplates() (*template.Template, error) {
|
|
// Define template functions
|
|
funcMap := template.FuncMap{
|
|
"activityClass": activityClass,
|
|
"statusClass": statusClass,
|
|
"workStatusClass": workStatusClass,
|
|
"progressPercent": progressPercent,
|
|
"senderColorClass": senderColorClass,
|
|
"severityClass": severityClass,
|
|
"dogStateClass": dogStateClass,
|
|
"queueStatusClass": queueStatusClass,
|
|
"polecatStatusClass": polecatStatusClass,
|
|
}
|
|
|
|
// Get the templates subdirectory
|
|
subFS, err := fs.Sub(templateFS, "templates")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Parse all templates
|
|
tmpl, err := template.New("").Funcs(funcMap).ParseFS(subFS, "*.html")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return tmpl, nil
|
|
}
|
|
|
|
// activityClass returns the CSS class for an activity color.
|
|
func activityClass(info activity.Info) string {
|
|
switch info.ColorClass {
|
|
case activity.ColorGreen:
|
|
return "activity-green"
|
|
case activity.ColorYellow:
|
|
return "activity-yellow"
|
|
case activity.ColorRed:
|
|
return "activity-red"
|
|
default:
|
|
return "activity-unknown"
|
|
}
|
|
}
|
|
|
|
// statusClass returns the CSS class for a convoy status.
|
|
func statusClass(status string) string {
|
|
switch status {
|
|
case "open":
|
|
return "status-open"
|
|
case "closed":
|
|
return "status-closed"
|
|
default:
|
|
return "status-unknown"
|
|
}
|
|
}
|
|
|
|
// 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 {
|
|
return 0
|
|
}
|
|
return (completed * 100) / total
|
|
}
|
|
|
|
// senderColorClass returns a CSS class for sender-based color coding.
|
|
// Uses a simple hash to assign consistent colors to each sender.
|
|
func senderColorClass(fromRaw string) string {
|
|
if fromRaw == "" {
|
|
return "sender-default"
|
|
}
|
|
// Simple hash: sum of bytes mod number of colors
|
|
var sum int
|
|
for _, b := range []byte(fromRaw) {
|
|
sum += int(b)
|
|
}
|
|
colors := []string{
|
|
"sender-cyan",
|
|
"sender-purple",
|
|
"sender-green",
|
|
"sender-yellow",
|
|
"sender-orange",
|
|
"sender-blue",
|
|
"sender-red",
|
|
"sender-pink",
|
|
}
|
|
return colors[sum%len(colors)]
|
|
}
|
|
|
|
// severityClass returns CSS class for escalation severity.
|
|
func severityClass(severity string) string {
|
|
switch severity {
|
|
case "critical":
|
|
return "severity-critical"
|
|
case "high":
|
|
return "severity-high"
|
|
case "medium":
|
|
return "severity-medium"
|
|
case "low":
|
|
return "severity-low"
|
|
default:
|
|
return "severity-unknown"
|
|
}
|
|
}
|
|
|
|
// dogStateClass returns CSS class for dog state.
|
|
func dogStateClass(state string) string {
|
|
switch state {
|
|
case "idle":
|
|
return "dog-idle"
|
|
case "working":
|
|
return "dog-working"
|
|
default:
|
|
return "dog-unknown"
|
|
}
|
|
}
|
|
|
|
// queueStatusClass returns CSS class for queue status.
|
|
func queueStatusClass(status string) string {
|
|
switch status {
|
|
case "active":
|
|
return "queue-active"
|
|
case "paused":
|
|
return "queue-paused"
|
|
case "closed":
|
|
return "queue-closed"
|
|
default:
|
|
return "queue-unknown"
|
|
}
|
|
}
|
|
|
|
// polecatStatusClass returns CSS class for polecat work status.
|
|
func polecatStatusClass(status string) string {
|
|
switch status {
|
|
case "working":
|
|
return "polecat-working"
|
|
case "stale":
|
|
return "polecat-stale"
|
|
case "stuck":
|
|
return "polecat-stuck"
|
|
case "idle":
|
|
return "polecat-idle"
|
|
default:
|
|
return "polecat-unknown"
|
|
}
|
|
}
|