feat(web): comprehensive dashboard control panel with 13 data panels (#931)

* 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.
This commit is contained in:
Clay Cantrell
2026-01-25 18:00:46 -08:00
committed by GitHub
parent 75739cbaaf
commit aca753296b
6 changed files with 3046 additions and 442 deletions
+243 -7
View File
@@ -14,9 +14,155 @@ var templateFS embed.FS
// ConvoyData represents data passed to the convoy template.
type ConvoyData struct {
Convoys []ConvoyRow
MergeQueue []MergeQueueRow
Polecats []PolecatRow
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.
@@ -26,6 +172,9 @@ type PolecatRow struct {
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.
@@ -64,10 +213,15 @@ type TrackedIssue struct {
func LoadTemplates() (*template.Template, error) {
// Define template functions
funcMap := template.FuncMap{
"activityClass": activityClass,
"statusClass": statusClass,
"workStatusClass": workStatusClass,
"progressPercent": progressPercent,
"activityClass": activityClass,
"statusClass": statusClass,
"workStatusClass": workStatusClass,
"progressPercent": progressPercent,
"senderColorClass": senderColorClass,
"severityClass": severityClass,
"dogStateClass": dogStateClass,
"queueStatusClass": queueStatusClass,
"polecatStatusClass": polecatStatusClass,
}
// Get the templates subdirectory
@@ -136,3 +290,85 @@ func progressPercent(completed, total int) int {
}
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"
}
}