summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorClaude Agent <agent@doot.local>2026-03-25 05:17:35 +0000
committerClaude Agent <agent@doot.local>2026-03-25 05:17:35 +0000
commitb58787cfec0bd07abc316c66dc9be6c10b8113c6 (patch)
treee1c788094f51bdab0bce8ad38c8d6638c9079bb9
parent2db5020047640361066510f29f908ca9fd1c99aa (diff)
feat: add Claudomator stories as atom source in Doot tasks tab
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
-rw-r--r--cmd/dashboard/main.go7
-rw-r--r--internal/api/claudomator.go54
-rw-r--r--internal/api/claudomator_test.go53
-rw-r--r--internal/api/interfaces.go6
-rw-r--r--internal/handlers/atoms.go15
-rw-r--r--internal/handlers/atoms_test.go47
-rw-r--r--internal/handlers/handlers.go6
-rw-r--r--internal/models/atom.go25
-rw-r--r--internal/models/atom_test.go31
-rw-r--r--internal/models/claudomator.go14
-rw-r--r--test/acceptance_test.go2
11 files changed, 251 insertions, 9 deletions
diff --git a/cmd/dashboard/main.go b/cmd/dashboard/main.go
index 437420d..09292b4 100644
--- a/cmd/dashboard/main.go
+++ b/cmd/dashboard/main.go
@@ -145,8 +145,13 @@ func main() {
}
}
+ var claudomatorClient api.ClaudomatorClient
+ if cfg.ClaudomatorURL != "" {
+ claudomatorClient = api.NewClaudomatorClient(cfg.ClaudomatorURL)
+ }
+
// Initialize handlers
- h := handlers.New(db, todoistClient, trelloClient, planToEatClient, googleCalendarClient, googleTasksClient, cfg, buildCommit, wa != nil)
+ h := handlers.New(db, todoistClient, trelloClient, planToEatClient, googleCalendarClient, googleTasksClient, claudomatorClient, cfg, buildCommit, wa != nil)
// Set up router
r := chi.NewRouter()
diff --git a/internal/api/claudomator.go b/internal/api/claudomator.go
new file mode 100644
index 0000000..3be4812
--- /dev/null
+++ b/internal/api/claudomator.go
@@ -0,0 +1,54 @@
+package api
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "time"
+
+ "task-dashboard/internal/models"
+)
+
+type ClaudomatorHTTPClient struct {
+ BaseURL string
+ HTTPClient *http.Client
+}
+
+func NewClaudomatorClient(baseURL string) *ClaudomatorHTTPClient {
+ return &ClaudomatorHTTPClient{
+ BaseURL: baseURL,
+ HTTPClient: &http.Client{Timeout: 5 * time.Second},
+ }
+}
+
+func (c *ClaudomatorHTTPClient) GetActiveStories(ctx context.Context) ([]models.ClaudomatorStory, error) {
+ req, err := http.NewRequestWithContext(ctx, "GET", c.BaseURL+"/api/stories", nil)
+ if err != nil {
+ return nil, err
+ }
+ httpClient := c.HTTPClient
+ if httpClient == nil {
+ httpClient = http.DefaultClient
+ }
+ resp, err := httpClient.Do(req)
+ if err != nil {
+ return nil, fmt.Errorf("claudomator: %w", err)
+ }
+ defer resp.Body.Close()
+ if resp.StatusCode != http.StatusOK {
+ return nil, fmt.Errorf("claudomator: unexpected status %d", resp.StatusCode)
+ }
+ var all []models.ClaudomatorStory
+ if err := json.NewDecoder(resp.Body).Decode(&all); err != nil {
+ return nil, err
+ }
+ active := make([]models.ClaudomatorStory, 0, len(all))
+ for _, s := range all {
+ switch s.Status {
+ case "IN_PROGRESS", "REVIEW_READY", "NEEDS_FIX":
+ active = append(active, s)
+ }
+ }
+ return active, nil
+}
diff --git a/internal/api/claudomator_test.go b/internal/api/claudomator_test.go
new file mode 100644
index 0000000..4a2cb00
--- /dev/null
+++ b/internal/api/claudomator_test.go
@@ -0,0 +1,53 @@
+package api
+
+import (
+ "context"
+ "encoding/json"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+
+ "task-dashboard/internal/models"
+)
+
+func TestClaudomatorClient_GetActiveStories(t *testing.T) {
+ stories := []models.ClaudomatorStory{
+ {ID: "1", Title: "Story One", Status: "IN_PROGRESS"},
+ {ID: "2", Title: "Story Two", Status: "REVIEW_READY"},
+ {ID: "3", Title: "Story Three", Status: "DRAFT"},
+ }
+
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if r.URL.Path != "/api/stories" {
+ http.NotFound(w, r)
+ return
+ }
+ w.Header().Set("Content-Type", "application/json")
+ _ = json.NewEncoder(w).Encode(stories)
+ }))
+ defer server.Close()
+
+ client := ClaudomatorHTTPClient{BaseURL: server.URL}
+ result, err := client.GetActiveStories(context.Background())
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+
+ if len(result) != 2 {
+ t.Fatalf("expected 2 active stories, got %d", len(result))
+ }
+
+ statuses := map[string]bool{}
+ for _, s := range result {
+ statuses[s.Status] = true
+ }
+ if !statuses["IN_PROGRESS"] {
+ t.Error("expected IN_PROGRESS story in results")
+ }
+ if !statuses["REVIEW_READY"] {
+ t.Error("expected REVIEW_READY story in results")
+ }
+ if statuses["DRAFT"] {
+ t.Error("DRAFT story should be excluded from results")
+ }
+}
diff --git a/internal/api/interfaces.go b/internal/api/interfaces.go
index 99701a1..0bd67b6 100644
--- a/internal/api/interfaces.go
+++ b/internal/api/interfaces.go
@@ -52,6 +52,11 @@ type GoogleTasksAPI interface {
SetTaskListID(id string)
}
+// ClaudomatorClient defines the interface for Claudomator operations
+type ClaudomatorClient interface {
+ GetActiveStories(ctx context.Context) ([]models.ClaudomatorStory, error)
+}
+
// Ensure concrete types implement interfaces
var (
_ TodoistAPI = (*TodoistClient)(nil)
@@ -59,4 +64,5 @@ var (
_ PlanToEatAPI = (*PlanToEatClient)(nil)
_ GoogleCalendarAPI = (*GoogleCalendarClient)(nil)
_ GoogleTasksAPI = (*GoogleTasksClient)(nil)
+ _ ClaudomatorClient = (*ClaudomatorHTTPClient)(nil)
)
diff --git a/internal/handlers/atoms.go b/internal/handlers/atoms.go
index e99c879..6086a5b 100644
--- a/internal/handlers/atoms.go
+++ b/internal/handlers/atoms.go
@@ -1,15 +1,17 @@
package handlers
import (
+ "context"
"log"
"sort"
+ "task-dashboard/internal/api"
"task-dashboard/internal/models"
"task-dashboard/internal/store"
)
// BuildUnifiedAtomList creates a list of atoms from tasks, cards, and google tasks
-func BuildUnifiedAtomList(s *store.Store) ([]models.Atom, []models.Board, error) {
+func BuildUnifiedAtomList(s *store.Store, claudomator api.ClaudomatorClient) ([]models.Atom, []models.Board, error) {
tasks, err := s.GetTasks()
if err != nil {
return nil, nil, err
@@ -51,6 +53,17 @@ func BuildUnifiedAtomList(s *store.Store) ([]models.Atom, []models.Board, error)
}
}
+ if claudomator != nil {
+ stories, err := claudomator.GetActiveStories(context.Background())
+ if err != nil {
+ log.Printf("Warning: failed to fetch Claudomator stories: %v", err)
+ } else {
+ for _, s := range stories {
+ atoms = append(atoms, models.StoryToAtom(s))
+ }
+ }
+ }
+
// Compute UI fields for all atoms
for i := range atoms {
atoms[i].ComputeUIFields()
diff --git a/internal/handlers/atoms_test.go b/internal/handlers/atoms_test.go
new file mode 100644
index 0000000..3be82f8
--- /dev/null
+++ b/internal/handlers/atoms_test.go
@@ -0,0 +1,47 @@
+package handlers
+
+import (
+ "context"
+ "testing"
+
+ "task-dashboard/internal/models"
+ "task-dashboard/internal/store"
+)
+
+type mockClaudomatorClient struct {
+ stories []models.ClaudomatorStory
+}
+
+func (m *mockClaudomatorClient) GetActiveStories(ctx context.Context) ([]models.ClaudomatorStory, error) {
+ return m.stories, nil
+}
+
+func TestBuildUnifiedAtomList_WithClaudomator(t *testing.T) {
+ s, err := store.New(":memory:", "../store/migrations")
+ if err != nil {
+ t.Fatalf("failed to create in-memory store: %v", err)
+ }
+ defer s.Close()
+
+ mock := &mockClaudomatorClient{
+ stories: []models.ClaudomatorStory{
+ {ID: "s1", Title: "My story", Status: "IN_PROGRESS", ProjectID: "nav"},
+ },
+ }
+
+ atoms, _, err := BuildUnifiedAtomList(s, mock)
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+
+ found := false
+ for _, a := range atoms {
+ if a.Source == "claudomator" && a.Title == "My story" {
+ found = true
+ break
+ }
+ }
+ if !found {
+ t.Error("expected atom with Source='claudomator' and Title='My story'")
+ }
+}
diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go
index fa97be0..bd14e65 100644
--- a/internal/handlers/handlers.go
+++ b/internal/handlers/handlers.go
@@ -27,6 +27,7 @@ type Handler struct {
planToEatClient api.PlanToEatAPI
googleCalendarClient api.GoogleCalendarAPI
googleTasksClient api.GoogleTasksAPI
+ claudomatorClient api.ClaudomatorClient
config *config.Config
renderer Renderer
BuildVersion string
@@ -34,7 +35,7 @@ type Handler struct {
}
// New creates a new Handler instance
-func New(s *store.Store, todoist api.TodoistAPI, trello api.TrelloAPI, planToEat api.PlanToEatAPI, googleCalendar api.GoogleCalendarAPI, googleTasks api.GoogleTasksAPI, cfg *config.Config, buildVersion string, webAuthnEnabled bool) *Handler {
+func New(s *store.Store, todoist api.TodoistAPI, trello api.TrelloAPI, planToEat api.PlanToEatAPI, googleCalendar api.GoogleCalendarAPI, googleTasks api.GoogleTasksAPI, claudomator api.ClaudomatorClient, cfg *config.Config, buildVersion string, webAuthnEnabled bool) *Handler {
// Template functions
funcMap := template.FuncMap{
"subtract": func(a, b int) int { return a - b },
@@ -59,6 +60,7 @@ func New(s *store.Store, todoist api.TodoistAPI, trello api.TrelloAPI, planToEat
planToEatClient: planToEat,
googleCalendarClient: googleCalendar,
googleTasksClient: googleTasks,
+ claudomatorClient: claudomator,
config: cfg,
renderer: NewTemplateRenderer(tmpl),
BuildVersion: buildVersion,
@@ -850,7 +852,7 @@ func (h *Handler) HandleUpdateTask(w http.ResponseWriter, r *http.Request) {
// HandleTabTasks renders the unified Tasks tab (Todoist + Trello cards with due dates + Bugs + Google Tasks)
func (h *Handler) HandleTabTasks(w http.ResponseWriter, r *http.Request) {
- atoms, boards, err := BuildUnifiedAtomList(h.store)
+ atoms, boards, err := BuildUnifiedAtomList(h.store, h.claudomatorClient)
if err != nil {
JSONError(w, http.StatusInternalServerError, "Failed to fetch tasks", err)
return
diff --git a/internal/models/atom.go b/internal/models/atom.go
index 767ccdd..3d9ce54 100644
--- a/internal/models/atom.go
+++ b/internal/models/atom.go
@@ -9,10 +9,11 @@ import (
type AtomSource string
const (
- SourceTrello AtomSource = "trello"
- SourceTodoist AtomSource = "todoist"
- SourceMeal AtomSource = "plantoeat"
- SourceGTasks AtomSource = "gtasks"
+ SourceTrello AtomSource = "trello"
+ SourceTodoist AtomSource = "todoist"
+ SourceMeal AtomSource = "plantoeat"
+ SourceGTasks AtomSource = "gtasks"
+ SourceClaudomator AtomSource = "claudomator"
)
type AtomType string
@@ -123,6 +124,22 @@ func CardToAtom(c Card) Atom {
}
}
+// StoryToAtom converts a ClaudomatorStory to an Atom
+func StoryToAtom(s ClaudomatorStory) Atom {
+ return Atom{
+ ID: s.ID,
+ Title: s.Title,
+ Description: s.Description + " [" + s.ProjectID + "]",
+ Source: SourceClaudomator,
+ Type: TypeTask,
+ Priority: 3,
+ SourceIcon: "🤖",
+ ColorClass: "border-purple-500",
+ CreatedAt: s.CreatedAt,
+ Raw: s,
+ }
+}
+
// GoogleTaskToAtom converts a Google Task to an Atom
func GoogleTaskToAtom(t GoogleTask) Atom {
// Google Tasks don't have explicit priority, default to medium (2)
diff --git a/internal/models/atom_test.go b/internal/models/atom_test.go
index 70bc14b..53bf343 100644
--- a/internal/models/atom_test.go
+++ b/internal/models/atom_test.go
@@ -262,6 +262,37 @@ func TestTimelineItem_ComputeDaySection(t *testing.T) {
}
}
+func TestStoryToAtom_Fields(t *testing.T) {
+ story := ClaudomatorStory{
+ ID: "s1",
+ Title: "Fix auth",
+ Description: "desc",
+ Status: "IN_PROGRESS",
+ ProjectID: "nav",
+ }
+
+ atom := StoryToAtom(story)
+
+ if atom.Source != SourceClaudomator {
+ t.Errorf("Expected source 'claudomator', got '%s'", atom.Source)
+ }
+ if atom.SourceIcon != "🤖" {
+ t.Errorf("Expected SourceIcon '🤖', got '%s'", atom.SourceIcon)
+ }
+ if atom.ColorClass != "border-purple-500" {
+ t.Errorf("Expected ColorClass 'border-purple-500', got '%s'", atom.ColorClass)
+ }
+ if atom.Priority != 3 {
+ t.Errorf("Expected Priority 3, got %d", atom.Priority)
+ }
+ if atom.Title != "Fix auth" {
+ t.Errorf("Expected Title 'Fix auth', got '%s'", atom.Title)
+ }
+ if atom.Description != "desc [nav]" {
+ t.Errorf("Expected Description 'desc [nav]', got '%s'", atom.Description)
+ }
+}
+
func TestCacheMetadata_IsCacheValid(t *testing.T) {
t.Run("valid cache", func(t *testing.T) {
cm := CacheMetadata{
diff --git a/internal/models/claudomator.go b/internal/models/claudomator.go
new file mode 100644
index 0000000..0883de3
--- /dev/null
+++ b/internal/models/claudomator.go
@@ -0,0 +1,14 @@
+package models
+
+import "time"
+
+type ClaudomatorStory struct {
+ ID string `json:"id"`
+ Title string `json:"title"`
+ Description string `json:"description"`
+ Status string `json:"status"`
+ ProjectID string `json:"project_id"`
+ BranchName string `json:"branch_name"`
+ CreatedAt time.Time `json:"created_at"`
+ UpdatedAt time.Time `json:"updated_at"`
+}
diff --git a/test/acceptance_test.go b/test/acceptance_test.go
index e561e98..d4a386b 100644
--- a/test/acceptance_test.go
+++ b/test/acceptance_test.go
@@ -82,7 +82,7 @@ func setupTestServer(t *testing.T) (*httptest.Server, *store.Store, *http.Client
}
// Initialize handlers
- h := handlers.New(db, todoistClient, trelloClient, nil, nil, nil, cfg, "test", false)
+ h := handlers.New(db, todoistClient, trelloClient, nil, nil, nil, nil, cfg, "test", false)
// Set up router (same as main.go)
r := chi.NewRouter()