Fix dashboard slowness by routing bd show to correct rig beads

The dashboard was taking 120+ seconds to respond due to two issues:

1. bd show commands for cross-rig issues ran from wrong beads directory,
   causing slow routing resolution (5.2s instead of 0.09s per lookup)

2. The bd daemon adds ~5s latency per command

Changes:
- Add routes map to LiveConvoyFetcher, loaded from routes.jsonl
- Add getBeadsDir() to determine correct beads directory for issue prefix
- Update getIssueDetailsBatch() to group issues by rig and run bd from
  the correct beads directory
- Add BEADS_NO_DAEMON=1 to bypass daemon for faster execution

Performance improvement:
- Before: 126+ seconds (timeout)
- After: ~5 seconds

Closes: hq-tyfkm
This commit is contained in:
2026-01-17 23:01:02 -08:00
committed by John Ogle
parent 3ffebb136f
commit 3d1dea5949

View File

@@ -15,7 +15,9 @@ import (
// LiveConvoyFetcher fetches convoy data from beads.
type LiveConvoyFetcher struct {
townRoot string
townBeads string
routes map[string]string // prefix -> rig path (e.g., "sc-" -> "scout")
}
// NewLiveConvoyFetcher creates a fetcher for the current workspace.
@@ -25,11 +27,60 @@ func NewLiveConvoyFetcher() (*LiveConvoyFetcher, error) {
return nil, fmt.Errorf("not in a Gas Town workspace: %w", err)
}
return &LiveConvoyFetcher{
fetcher := &LiveConvoyFetcher{
townRoot: townRoot,
townBeads: filepath.Join(townRoot, ".beads"),
}, nil
routes: make(map[string]string),
}
// Load routes from routes.jsonl
fetcher.loadRoutes()
return fetcher, nil
}
// loadRoutes loads prefix routing from routes.jsonl.
func (f *LiveConvoyFetcher) loadRoutes() {
routesFile := filepath.Join(f.townBeads, "routes.jsonl")
data, err := exec.Command("cat", routesFile).Output()
if err != nil {
return
}
for _, line := range strings.Split(string(data), "\n") {
line = strings.TrimSpace(line)
if line == "" {
continue
}
var route struct {
Prefix string `json:"prefix"`
Path string `json:"path"`
}
if err := json.Unmarshal([]byte(line), &route); err == nil && route.Prefix != "" {
f.routes[route.Prefix] = route.Path
}
}
}
// getBeadsDir returns the appropriate beads directory for an issue ID.
// Routes issues to the correct rig's beads based on prefix.
func (f *LiveConvoyFetcher) getBeadsDir(issueID string) string {
// Find longest matching prefix
var bestMatch string
var bestPath string
for prefix, path := range f.routes {
if strings.HasPrefix(issueID, prefix) && len(prefix) > len(bestMatch) {
bestMatch = prefix
bestPath = path
}
}
if bestPath == "" || bestPath == "." {
return f.townBeads
}
return filepath.Join(f.townRoot, bestPath, ".beads")
}
// FetchConvoys fetches all open convoys with their activity data.
func (f *LiveConvoyFetcher) FetchConvoys() ([]ConvoyRow, error) {
@@ -37,6 +88,8 @@ func (f *LiveConvoyFetcher) FetchConvoys() ([]ConvoyRow, error) {
listArgs := []string{"list", "--type=convoy", "--status=open", "--json"}
listCmd := exec.Command("bd", listArgs...)
listCmd.Dir = f.townBeads
// Bypass daemon for faster execution (daemon adds ~5s latency)
listCmd.Env = append(listCmd.Environ(), "BEADS_NO_DAEMON=1")
var stdout bytes.Buffer
listCmd.Stdout = &stdout
@@ -228,49 +281,63 @@ type issueDetail struct {
}
// getIssueDetailsBatch fetches details for multiple issues.
// Groups issues by their rig prefix and fetches from the correct beads directory.
func (f *LiveConvoyFetcher) getIssueDetailsBatch(issueIDs []string) map[string]*issueDetail {
result := make(map[string]*issueDetail)
if len(issueIDs) == 0 {
return result
}
args := append([]string{"show"}, issueIDs...)
args = append(args, "--json")
// #nosec G204 -- bd is a trusted internal tool, args are issue IDs
showCmd := exec.Command("bd", args...)
var stdout bytes.Buffer
showCmd.Stdout = &stdout
if err := showCmd.Run(); err != nil {
return result
// Group issues by their beads directory
byDir := make(map[string][]string)
for _, id := range issueIDs {
dir := f.getBeadsDir(id)
byDir[dir] = append(byDir[dir], id)
}
var issues []struct {
ID string `json:"id"`
Title string `json:"title"`
Status string `json:"status"`
Assignee string `json:"assignee"`
UpdatedAt string `json:"updated_at"`
}
if err := json.Unmarshal(stdout.Bytes(), &issues); err != nil {
return result
}
// Fetch from each directory
for dir, ids := range byDir {
args := append([]string{"show"}, ids...)
args = append(args, "--json")
for _, issue := range issues {
detail := &issueDetail{
ID: issue.ID,
Title: issue.Title,
Status: issue.Status,
Assignee: issue.Assignee,
// #nosec G204 -- bd is a trusted internal tool, args are issue IDs
showCmd := exec.Command("bd", args...)
showCmd.Dir = dir
// Bypass daemon for faster execution (daemon adds ~5s latency)
showCmd.Env = append(showCmd.Environ(), "BEADS_NO_DAEMON=1")
var stdout bytes.Buffer
showCmd.Stdout = &stdout
if err := showCmd.Run(); err != nil {
continue
}
// Parse updated_at timestamp
if issue.UpdatedAt != "" {
if t, err := time.Parse(time.RFC3339, issue.UpdatedAt); err == nil {
detail.UpdatedAt = t
var issues []struct {
ID string `json:"id"`
Title string `json:"title"`
Status string `json:"status"`
Assignee string `json:"assignee"`
UpdatedAt string `json:"updated_at"`
}
if err := json.Unmarshal(stdout.Bytes(), &issues); err != nil {
continue
}
for _, issue := range issues {
detail := &issueDetail{
ID: issue.ID,
Title: issue.Title,
Status: issue.Status,
Assignee: issue.Assignee,
}
// Parse updated_at timestamp
if issue.UpdatedAt != "" {
if t, err := time.Parse(time.RFC3339, issue.UpdatedAt); err == nil {
detail.UpdatedAt = t
}
}
result[issue.ID] = detail
}
result[issue.ID] = detail
}
return result