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:
+243
-7
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user