summaryrefslogtreecommitdiff
path: root/internal/handlers
diff options
context:
space:
mode:
Diffstat (limited to 'internal/handlers')
-rw-r--r--internal/handlers/agent.go38
-rw-r--r--internal/handlers/agent_test.go26
-rw-r--r--internal/handlers/atoms.go14
-rw-r--r--internal/handlers/handlers.go252
-rw-r--r--internal/handlers/handlers_test.go802
-rw-r--r--internal/handlers/settings.go25
-rw-r--r--internal/handlers/tab_state_test.go7
-rw-r--r--internal/handlers/timeline_logic.go49
-rw-r--r--internal/handlers/timeline_logic_test.go146
-rw-r--r--internal/handlers/widget.go11
-rw-r--r--internal/handlers/widget_test.go8
11 files changed, 96 insertions, 1282 deletions
diff --git a/internal/handlers/agent.go b/internal/handlers/agent.go
index 826ffd7..073084f 100644
--- a/internal/handlers/agent.go
+++ b/internal/handlers/agent.go
@@ -414,12 +414,6 @@ func (h *Handler) handleAgentTaskToggle(w http.ResponseWriter, r *http.Request,
var err error
ctx := r.Context()
switch source {
- case "todoist":
- if complete {
- err = h.todoistClient.CompleteTask(ctx, id)
- } else {
- err = h.todoistClient.ReopenTask(ctx, id)
- }
case "trello":
err = h.trelloClient.UpdateCard(ctx, id, map[string]interface{}{"closed": complete})
case "gtasks":
@@ -450,17 +444,11 @@ func (h *Handler) handleAgentTaskToggle(w http.ResponseWriter, r *http.Request,
if complete {
title, dueDate := h.getAtomDetails(id, source)
_ = h.store.SaveCompletedTask(source, id, title, dueDate)
- switch source {
- case "todoist":
- _ = h.store.DeleteTask(id)
- case "trello":
+ if source == "trello" {
_ = h.store.DeleteCard(id)
}
} else {
- switch source {
- case "todoist":
- _ = h.store.InvalidateCache(store.CacheKeyTodoistTasks)
- case "trello":
+ if source == "trello" {
_ = h.store.InvalidateCache(store.CacheKeyTrelloBoards)
}
}
@@ -485,8 +473,6 @@ func (h *Handler) HandleAgentTaskUpdateDue(w http.ResponseWriter, r *http.Reques
var err error
ctx := r.Context()
switch source {
- case "todoist":
- err = h.todoistClient.UpdateTask(ctx, id, map[string]interface{}{"due_datetime": req.Due})
case "trello":
err = h.trelloClient.UpdateCard(ctx, id, map[string]interface{}{"due": req.Due})
default:
@@ -500,10 +486,7 @@ func (h *Handler) HandleAgentTaskUpdateDue(w http.ResponseWriter, r *http.Reques
}
// Invalidate cache
- switch source {
- case "todoist":
- _ = h.store.InvalidateCache(store.CacheKeyTodoistTasks)
- case "trello":
+ if source == "trello" {
_ = h.store.InvalidateCache(store.CacheKeyTrelloBoards)
}
@@ -525,13 +508,6 @@ func (h *Handler) HandleAgentTaskUpdate(w http.ResponseWriter, r *http.Request)
var err error
ctx := r.Context()
switch source {
- case "todoist":
- // Map generic updates to Todoist specific ones if needed
- if title, ok := updates["title"].(string); ok {
- updates["content"] = title
- delete(updates, "title")
- }
- err = h.todoistClient.UpdateTask(ctx, id, updates)
case "trello":
if title, ok := updates["title"].(string); ok {
updates["name"] = title
@@ -553,10 +529,7 @@ func (h *Handler) HandleAgentTaskUpdate(w http.ResponseWriter, r *http.Request)
}
// Invalidate cache
- switch source {
- case "todoist":
- _ = h.store.InvalidateCache(store.CacheKeyTodoistTasks)
- case "trello":
+ if source == "trello" {
_ = h.store.InvalidateCache(store.CacheKeyTrelloBoards)
}
@@ -587,9 +560,6 @@ func (h *Handler) HandleAgentTaskCreate(w http.ResponseWriter, r *http.Request)
var err error
ctx := r.Context()
switch req.Source {
- case "todoist":
- _, err = h.todoistClient.CreateTask(ctx, req.Title, req.ProjectID, req.DueDate, req.Priority)
- _ = h.store.InvalidateCache(store.CacheKeyTodoistTasks)
case "trello":
if req.ListID == "" {
http.Error(w, "list_id is required for Trello", http.StatusBadRequest)
diff --git a/internal/handlers/agent_test.go b/internal/handlers/agent_test.go
index eab1609..2893a13 100644
--- a/internal/handlers/agent_test.go
+++ b/internal/handlers/agent_test.go
@@ -746,13 +746,11 @@ func TestHandleAgentTaskWriteOperations(t *testing.T) {
defer cleanup()
// Mock clients
- mockTodoist := &mockTodoistClient{}
mockTrello := &mockTrelloClient{}
h := &Handler{
- store: db,
- todoistClient: mockTodoist,
- trelloClient: mockTrello,
- config: &config.Config{},
+ store: db,
+ trelloClient: mockTrello,
+ config: &config.Config{},
}
// Create an approved session
@@ -766,11 +764,11 @@ func TestHandleAgentTaskWriteOperations(t *testing.T) {
db.CreateAgentSession(session)
db.ApproveAgentSession("write-request-token", sessionToken, time.Now().Add(1*time.Hour))
- // Pre-populate store with a task to get details during completion
- db.SaveTasks([]models.Task{{ID: "task123", Content: "Test Task"}})
+ // Pre-populate store with a native task
+ _ = db.CreateNativeTask(models.Task{ID: "task123", Content: "Test Task"})
t.Run("complete task", func(t *testing.T) {
- req := httptest.NewRequest(http.MethodPost, "/agent/tasks/task123/complete?source=todoist", nil)
+ req := httptest.NewRequest(http.MethodPost, "/agent/tasks/task123/complete?source=doot", nil)
w := httptest.NewRecorder()
// Manually inject session into context as middleware would
@@ -804,7 +802,7 @@ func TestHandleAgentTaskWriteOperations(t *testing.T) {
t.Run("update due date", func(t *testing.T) {
due := time.Now().Add(24 * time.Hour)
body, _ := json.Marshal(map[string]*time.Time{"due": &due})
- req := httptest.NewRequest(http.MethodPatch, "/agent/tasks/task123/due?source=todoist", bytes.NewReader(body))
+ req := httptest.NewRequest(http.MethodPatch, "/agent/tasks/task123/due?source=doot", bytes.NewReader(body))
w := httptest.NewRecorder()
rctx := chi.NewRouteContext()
@@ -820,7 +818,7 @@ func TestHandleAgentTaskWriteOperations(t *testing.T) {
t.Run("update task details", func(t *testing.T) {
body, _ := json.Marshal(map[string]string{"title": "Updated Title", "description": "New Desc"})
- req := httptest.NewRequest(http.MethodPatch, "/agent/tasks/task123?source=todoist", bytes.NewReader(body))
+ req := httptest.NewRequest(http.MethodPatch, "/agent/tasks/task123?source=doot", bytes.NewReader(body))
w := httptest.NewRecorder()
rctx := chi.NewRouteContext()
@@ -839,17 +837,15 @@ func TestHandleAgentCreateOperations(t *testing.T) {
db, cleanup := setupTestDB(t)
defer cleanup()
- mockTodoist := &mockTodoistClient{}
h := &Handler{
- store: db,
- todoistClient: mockTodoist,
- config: &config.Config{},
+ store: db,
+ config: &config.Config{},
}
t.Run("create task", func(t *testing.T) {
body, _ := json.Marshal(map[string]string{
"title": "New Agent Task",
- "source": "todoist",
+ "source": "doot",
})
req := httptest.NewRequest(http.MethodPost, "/agent/tasks", bytes.NewReader(body))
w := httptest.NewRecorder()
diff --git a/internal/handlers/atoms.go b/internal/handlers/atoms.go
index 0b59060..fe092a3 100644
--- a/internal/handlers/atoms.go
+++ b/internal/handlers/atoms.go
@@ -12,11 +12,6 @@ import (
// BuildUnifiedAtomList creates a list of atoms from tasks, cards, and google tasks
func BuildUnifiedAtomList(s *store.Store, claudomator api.ClaudomatorClient) ([]models.Atom, []models.Board, error) {
- tasks, err := s.GetTasks()
- if err != nil {
- return nil, nil, err
- }
-
boards, err := s.GetBoards()
if err != nil {
return nil, nil, err
@@ -33,20 +28,13 @@ func BuildUnifiedAtomList(s *store.Store, claudomator api.ClaudomatorClient) ([]
log.Printf("Warning: failed to fetch native tasks: %v", err)
}
- atoms := make([]models.Atom, 0, len(tasks)+len(gTasks)+len(nativeTasks))
+ atoms := make([]models.Atom, 0, len(gTasks)+len(nativeTasks))
// Add native doot tasks (GetNativeTasks already filters completed=0)
for _, task := range nativeTasks {
atoms = append(atoms, models.NativeTaskToAtom(task))
}
- // Add incomplete tasks
- for _, task := range tasks {
- if !task.Completed {
- atoms = append(atoms, models.TaskToAtom(task))
- }
- }
-
// Add incomplete google tasks
for _, gTask := range gTasks {
if !gTask.Completed {
diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go
index 6c74fff..d1a7512 100644
--- a/internal/handlers/handlers.go
+++ b/internal/handlers/handlers.go
@@ -30,7 +30,6 @@ func newID() string {
// Handler holds dependencies for HTTP handlers
type Handler struct {
store *store.Store
- todoistClient api.TodoistAPI
trelloClient api.TrelloAPI
planToEatClient api.PlanToEatAPI
googleCalendarClient api.GoogleCalendarAPI
@@ -43,7 +42,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, claudomator api.ClaudomatorClient, cfg *config.Config, buildVersion string, webAuthnEnabled bool) *Handler {
+func New(s *store.Store, 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 },
@@ -63,7 +62,6 @@ func New(s *store.Store, todoist api.TodoistAPI, trello api.TrelloAPI, planToEat
return &Handler{
store: s,
- todoistClient: todoist,
trelloClient: trello,
planToEatClient: planToEat,
googleCalendarClient: googleCalendar,
@@ -134,16 +132,6 @@ func (h *Handler) HandleRefresh(w http.ResponseWriter, r *http.Request) {
JSONResponse(w, data)
}
-// HandleGetTasks returns tasks as JSON
-func (h *Handler) HandleGetTasks(w http.ResponseWriter, r *http.Request) {
- tasks, err := h.store.GetTasks()
- if err != nil {
- JSONError(w, http.StatusInternalServerError, "Failed to get tasks", err)
- return
- }
- JSONResponse(w, tasks)
-}
-
// HandleGetMeals returns meals as JSON
func (h *Handler) HandleGetMeals(w http.ResponseWriter, r *http.Request) {
startDate := config.Now()
@@ -221,17 +209,6 @@ func (h *Handler) aggregateData(ctx context.Context, forceRefresh bool) (*models
return err
})
- fetch("Todoist", func() error {
- tasks, err := h.fetchTasks(ctx, forceRefresh)
- if err == nil {
- sortTasksByUrgency(tasks)
- mu.Lock()
- data.Tasks = tasks
- mu.Unlock()
- }
- return err
- })
-
if h.planToEatClient != nil {
fetch("PlanToEat", func() error {
meals, err := h.fetchMeals(ctx, forceRefresh)
@@ -270,35 +247,12 @@ func (h *Handler) aggregateData(ctx context.Context, forceRefresh bool) (*models
wg.Wait()
- // Populate projects from cached tasks (avoids deprecated REST API)
- if projects, err := h.store.GetProjectsFromTasks(); err == nil {
- data.Projects = projects
- }
-
// Extract and sort Trello tasks
data.TrelloTasks = filterAndSortTrelloTasks(data.Boards)
return data, nil
}
-// sortTasksByUrgency sorts tasks by due date then priority
-func sortTasksByUrgency(tasks []models.Task) {
- sort.Slice(tasks, func(i, j int) bool {
- if tasks[i].DueDate == nil && tasks[j].DueDate != nil {
- return false
- }
- if tasks[i].DueDate != nil && tasks[j].DueDate == nil {
- return true
- }
- if tasks[i].DueDate != nil && tasks[j].DueDate != nil {
- if !tasks[i].DueDate.Equal(*tasks[j].DueDate) {
- return tasks[i].DueDate.Before(*tasks[j].DueDate)
- }
- }
- return tasks[i].Priority > tasks[j].Priority
- })
-}
-
// filterAndSortTrelloTasks extracts task-like cards from boards
func filterAndSortTrelloTasks(boards []models.Board) []models.Card {
var tasks []models.Card
@@ -328,39 +282,6 @@ func filterAndSortTrelloTasks(boards []models.Board) []models.Card {
return tasks
}
-// fetchTasks fetches tasks from cache or API
-func (h *Handler) fetchTasks(ctx context.Context, forceRefresh bool) ([]models.Task, error) {
- cacheKey := store.CacheKeyTodoistTasks
-
- // Check cache validity
- if !forceRefresh {
- valid, err := h.store.IsCacheValid(cacheKey)
- if err == nil && valid {
- return h.store.GetTasks()
- }
- }
-
- tasks, err := h.todoistClient.GetTasks(ctx)
- if err != nil {
- // Fall back to cached data if available
- cachedTasks, cacheErr := h.store.GetTasks()
- if cacheErr == nil && len(cachedTasks) > 0 {
- return cachedTasks, nil
- }
- return nil, err
- }
-
- if err := h.store.SaveTasks(tasks); err != nil {
- log.Printf("Failed to save tasks to cache: %v", err)
- }
-
- if err := h.store.UpdateCacheMetadata(cacheKey, h.config.CacheTTLMinutes); err != nil {
- log.Printf("Failed to update cache metadata: %v", err)
- }
-
- return tasks, nil
-}
-
// fetchMeals fetches meals from cache or API
func (h *Handler) fetchMeals(ctx context.Context, forceRefresh bool) ([]models.Meal, error) {
startDate := config.Now()
@@ -534,52 +455,6 @@ func (h *Handler) HandleCompleteCard(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}
-// HandleCreateTask creates a new Todoist task
-func (h *Handler) HandleCreateTask(w http.ResponseWriter, r *http.Request) {
- ctx := r.Context()
-
- content, ok := requireFormValue(w, r, "content")
- if !ok {
- return
- }
- projectID := r.FormValue("project_id")
-
- if _, err := h.todoistClient.CreateTask(ctx, content, projectID, nil, 0); err != nil {
- JSONError(w, http.StatusInternalServerError, "Failed to create task", err)
- return
- }
-
- tasks, err := h.fetchTasks(ctx, true)
- if err != nil {
- JSONError(w, http.StatusInternalServerError, "Failed to refresh tasks", err)
- return
- }
-
- projects, _ := h.store.GetProjectsFromTasks()
-
- data := struct {
- Tasks []models.Task
- Projects []models.Project
- }{Tasks: tasks, Projects: projects}
-
- HTMLResponse(w, h.renderer, "todoist-tasks", data)
-}
-
-// HandleCompleteTask marks a Todoist task as complete
-func (h *Handler) HandleCompleteTask(w http.ResponseWriter, r *http.Request) {
- taskID, ok := requireFormValue(w, r, "task_id")
- if !ok {
- return
- }
-
- if err := h.todoistClient.CompleteTask(r.Context(), taskID); err != nil {
- JSONError(w, http.StatusInternalServerError, "Failed to complete task", err)
- return
- }
-
- w.WriteHeader(http.StatusOK)
-}
-
// HandleCompleteAtom handles completion of a unified task (Atom)
func (h *Handler) HandleCompleteAtom(w http.ResponseWriter, r *http.Request) {
h.handleAtomToggle(w, r, true)
@@ -623,16 +498,6 @@ func (h *Handler) handleAtomToggle(w http.ResponseWriter, r *http.Request, compl
} else {
err = h.store.UncompleteNativeTask(id)
}
- case "todoist":
- if h.todoistClient == nil {
- JSONError(w, http.StatusServiceUnavailable, "Todoist not configured", nil)
- return
- }
- if complete {
- err = h.todoistClient.CompleteTask(ctx, id)
- } else {
- err = h.todoistClient.ReopenTask(ctx, id)
- }
case "trello":
err = h.trelloClient.UpdateCard(ctx, id, map[string]interface{}{"closed": complete})
case "gtasks":
@@ -674,8 +539,6 @@ func (h *Handler) handleAtomToggle(w http.ResponseWriter, r *http.Request, compl
// Remove from local cache
switch source {
- case "todoist":
- _ = h.store.DeleteTask(id)
case "trello":
_ = h.store.DeleteCard(id)
case "doot":
@@ -691,10 +554,7 @@ func (h *Handler) handleAtomToggle(w http.ResponseWriter, r *http.Request, compl
HTMLResponse(w, h.renderer, "completed-atom", data)
} else {
// Invalidate cache to force refresh
- switch source {
- case "todoist":
- _ = h.store.InvalidateCache(store.CacheKeyTodoistTasks)
- case "trello":
+ if source == "trello" {
_ = h.store.InvalidateCache(store.CacheKeyTrelloBoards)
}
// Don't swap empty response - just trigger refresh
@@ -715,14 +575,6 @@ func (h *Handler) getAtomDetails(id, source string) (string, *time.Time) {
}
}
}
- case "todoist":
- if tasks, err := h.store.GetTasks(); err == nil {
- for _, t := range tasks {
- if t.ID == id {
- return t.Content, t.DueDate
- }
- }
- }
case "trello":
if boards, err := h.store.GetBoards(); err == nil {
for _, b := range boards {
@@ -739,7 +591,7 @@ func (h *Handler) getAtomDetails(id, source string) (string, *time.Time) {
return "Task", nil
}
-// HandleUnifiedAdd creates a task in Todoist or a card in Trello from the Quick Add form
+// HandleUnifiedAdd creates a native task or a card in Trello from the Quick Add form
func (h *Handler) HandleUnifiedAdd(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
@@ -767,25 +619,17 @@ func (h *Handler) HandleUnifiedAdd(w http.ResponseWriter, r *http.Request) {
switch source {
case "doot":
task := models.Task{
- ID: newID(),
- Content: title,
- DueDate: dueDate,
- Priority: 1,
- Labels: nil,
+ ID: newID(),
+ Content: title,
+ DueDate: dueDate,
+ Priority: 1,
+ Labels: nil,
}
if err := h.store.CreateNativeTask(task); err != nil {
JSONError(w, http.StatusInternalServerError, "Failed to create task", err)
return
}
- case "todoist":
- projectID := r.FormValue("project_id")
- if _, err := h.todoistClient.CreateTask(ctx, title, projectID, dueDate, 1); err != nil {
- JSONError(w, http.StatusInternalServerError, "Failed to create Todoist task", err)
- return
- }
- _ = h.store.InvalidateCache(store.CacheKeyTodoistTasks)
-
case "trello":
listID := r.FormValue("list_id")
if listID == "" {
@@ -836,15 +680,6 @@ func (h *Handler) HandleGetTaskDetail(w http.ResponseWriter, r *http.Request) {
var title, description string
switch source {
- case "todoist":
- if tasks, err := h.store.GetTasks(); err == nil {
- for _, t := range tasks {
- if t.ID == id {
- title, description = t.Content, t.Description
- break
- }
- }
- }
case "trello":
if boards, err := h.store.GetBoards(); err == nil {
for _, b := range boards {
@@ -887,15 +722,6 @@ func (h *Handler) HandleTaskDetailPage(w http.ResponseWriter, r *http.Request) {
}
}
}
- case "todoist":
- if tasks, err := h.store.GetTasks(); err == nil {
- for _, t := range tasks {
- if t.ID == id {
- title, description = t.Content, t.Description
- break
- }
- }
- }
case "trello":
if boards, err := h.store.GetBoards(); err == nil {
for _, b := range boards {
@@ -947,8 +773,6 @@ func (h *Handler) HandleUpdateTask(w http.ResponseWriter, r *http.Request) {
switch source {
case "doot":
err = h.store.UpdateNativeTask(id, "", description)
- case "todoist":
- err = h.todoistClient.UpdateTask(r.Context(), id, map[string]interface{}{"description": description})
case "trello":
err = h.trelloClient.UpdateCard(r.Context(), id, map[string]interface{}{"desc": description})
default:
@@ -968,18 +792,7 @@ func (h *Handler) HandleUpdateTask(w http.ResponseWriter, r *http.Request) {
}
}
-// HandleImportFromTodoist copies the current Todoist task cache into native_tasks.
-// One-time migration action. Safe to run multiple times (INSERT OR IGNORE by ID).
-func (h *Handler) HandleImportFromTodoist(w http.ResponseWriter, r *http.Request) {
- count, err := h.store.ImportFromTodoist()
- if err != nil {
- http.Error(w, "Import failed: "+err.Error(), http.StatusInternalServerError)
- return
- }
- fmt.Fprintf(w, "Imported %d tasks from Todoist. You can now remove TODOIST_TOKEN from your .env to disable Todoist.", count)
-}
-
-// HandleTabTasks renders the unified Tasks tab (Todoist + Trello cards with due dates + Bugs + Google Tasks)
+// HandleTabTasks renders the unified Tasks tab (native tasks + Trello cards with due dates + Google Tasks)
func (h *Handler) HandleTabTasks(w http.ResponseWriter, r *http.Request) {
atoms, boards, err := BuildUnifiedAtomList(h.store, h.claudomatorClient)
if err != nil {
@@ -990,19 +803,15 @@ func (h *Handler) HandleTabTasks(w http.ResponseWriter, r *http.Request) {
SortAtomsByUrgency(atoms)
currentAtoms, futureAtoms := PartitionAtomsByTime(atoms)
- projects, _ := h.store.GetProjectsFromTasks()
-
data := struct {
Atoms []models.Atom
FutureAtoms []models.Atom
Boards []models.Board
- Projects []models.Project
Today string
}{
Atoms: currentAtoms,
FutureAtoms: futureAtoms,
Boards: boards,
- Projects: projects,
Today: config.Now().Format("2006-01-02"),
}
@@ -1016,7 +825,6 @@ func (h *Handler) HandleTabPlanning(w http.ResponseWriter, r *http.Request) {
in3Days := today.AddDate(0, 0, 4)
boards, _ := h.store.GetBoards()
- tasks, _ := h.store.GetTasks()
gTasks, _ := h.store.GetGoogleTasks()
events, _ := h.fetchCalendarEvents(r.Context(), false)
@@ -1044,44 +852,6 @@ func (h *Handler) HandleTabPlanning(w http.ResponseWriter, r *http.Request) {
}
}
- for _, task := range tasks {
- if task.Completed || task.DueDate == nil {
- continue
- }
- dueDate := *task.DueDate
- hasTime := dueDate.Hour() != 0 || dueDate.Minute() != 0
-
- if dueDate.Before(tomorrow) {
- if hasTime {
- scheduled = append(scheduled, ScheduledItem{
- Type: "task",
- ID: task.ID,
- Title: task.Content,
- Start: dueDate,
- URL: task.URL,
- Source: "todoist",
- SourceIcon: "✓",
- Priority: task.Priority,
- })
- } else {
- atom := models.TaskToAtom(task)
- atom.ComputeUIFields()
- unscheduled = append(unscheduled, atom)
- }
- } else if dueDate.Before(in3Days) {
- upcoming = append(upcoming, ScheduledItem{
- Type: "task",
- ID: task.ID,
- Title: task.Content,
- Start: dueDate,
- URL: task.URL,
- Source: "todoist",
- SourceIcon: "✓",
- Priority: task.Priority,
- })
- }
- }
-
for _, board := range boards {
for _, card := range board.Cards {
if card.DueDate == nil {
@@ -1171,21 +941,17 @@ func (h *Handler) HandleTabPlanning(w http.ResponseWriter, r *http.Request) {
sort.Slice(unscheduled, func(i, j int) bool { return unscheduled[i].Priority > unscheduled[j].Priority })
sort.Slice(upcoming, func(i, j int) bool { return upcoming[i].Start.Before(upcoming[j].Start) })
- projects, _ := h.store.GetProjectsFromTasks()
-
data := struct {
Scheduled []ScheduledItem
Unscheduled []models.Atom
Upcoming []ScheduledItem
Boards []models.Board
- Projects []models.Project
Today string
}{
Scheduled: scheduled,
Unscheduled: unscheduled,
Upcoming: upcoming,
Boards: boards,
- Projects: projects,
Today: today.Format("2006-01-02"),
}
diff --git a/internal/handlers/handlers_test.go b/internal/handlers/handlers_test.go
index b640c78..1f66378 100644
--- a/internal/handlers/handlers_test.go
+++ b/internal/handlers/handlers_test.go
@@ -84,40 +84,6 @@ func setupTestHandler(t *testing.T) (*Handler, func()) {
return h, cleanup
}
-// mockTodoistClient creates a mock Todoist client for testing
-type mockTodoistClient struct {
- tasks []models.Task
- err error
-}
-
-func (m *mockTodoistClient) GetTasks(ctx context.Context) ([]models.Task, error) {
- if m.err != nil {
- return nil, m.err
- }
- return m.tasks, nil
-}
-
-func (m *mockTodoistClient) GetProjects(ctx context.Context) ([]models.Project, error) {
- return []models.Project{}, nil
-}
-
-func (m *mockTodoistClient) CreateTask(ctx context.Context, content, projectID string, dueDate *time.Time, priority int) (*models.Task, error) {
- return nil, nil
-}
-
-func (m *mockTodoistClient) UpdateTask(ctx context.Context, taskID string, updates map[string]interface{}) error {
- return m.err
-}
-
-func (m *mockTodoistClient) CompleteTask(ctx context.Context, taskID string) error {
- return nil
-}
-
-func (m *mockTodoistClient) ReopenTask(ctx context.Context, taskID string) error {
- return nil
-}
-
-
// mockTrelloClient creates a mock Trello client for testing
type mockTrelloClient struct {
boards []models.Board
@@ -154,79 +120,6 @@ func (m *mockTrelloClient) UpdateCard(ctx context.Context, cardID string, update
return nil
}
-// TestHandleGetTasks tests the HandleGetTasks handler
-func TestHandleGetTasks(t *testing.T) {
- db, cleanup := setupTestDB(t)
- defer cleanup()
-
- // Create test tasks
- testTasks := []models.Task{
- {
- ID: "1",
- Content: "Test task 1",
- Description: "Description 1",
- ProjectID: "proj1",
- ProjectName: "Project 1",
- Priority: 1,
- Completed: false,
- Labels: []string{"label1"},
- URL: "https://todoist.com/task/1",
- CreatedAt: time.Now(),
- },
- {
- ID: "2",
- Content: "Test task 2",
- Description: "Description 2",
- ProjectID: "proj2",
- ProjectName: "Project 2",
- Priority: 2,
- Completed: true,
- Labels: []string{"label2"},
- URL: "https://todoist.com/task/2",
- CreatedAt: time.Now(),
- },
- }
-
- // Save tasks to database
- if err := db.SaveTasks(testTasks); err != nil {
- t.Fatalf("Failed to save test tasks: %v", err)
- }
-
- // Create handler with mock client
- cfg := &config.Config{
- CacheTTLMinutes: 5,
- }
- mockTodoist := &mockTodoistClient{}
- h := &Handler{
- store: db,
- todoistClient: mockTodoist,
- config: cfg,
- }
-
- // Create test request
- req := httptest.NewRequest("GET", "/api/tasks", nil)
- w := httptest.NewRecorder()
-
- // Execute handler
- h.HandleGetTasks(w, req)
-
- // Check response
- if w.Code != http.StatusOK {
- t.Errorf("Expected status 200, got %d", w.Code)
- }
-
- // Parse response
- var tasks []models.Task
- if err := json.NewDecoder(w.Body).Decode(&tasks); err != nil {
- t.Fatalf("Failed to decode response: %v", err)
- }
-
- // Verify tasks
- if len(tasks) != 2 {
- t.Errorf("Expected 2 tasks, got %d", len(tasks))
- }
-}
-
// TestHandleGetBoards tests the HandleGetBoards handler
func TestHandleGetBoards(t *testing.T) {
db, cleanup := setupTestDB(t)
@@ -309,16 +202,6 @@ func TestHandleRefresh(t *testing.T) {
db, cleanup := setupTestDB(t)
defer cleanup()
- // Create mock clients
- mockTodoist := &mockTodoistClient{
- tasks: []models.Task{
- {
- ID: "1",
- Content: "Test task",
- },
- },
- }
-
mockTrello := &mockTrelloClient{
boards: []models.Board{
{
@@ -333,10 +216,9 @@ func TestHandleRefresh(t *testing.T) {
CacheTTLMinutes: 5,
}
h := &Handler{
- store: db,
- todoistClient: mockTodoist,
- trelloClient: mockTrello,
- config: cfg,
+ store: db,
+ trelloClient: mockTrello,
+ config: cfg,
}
// Create test request
@@ -397,21 +279,6 @@ func TestHandleGetMeals(t *testing.T) {
}
}
-// mockTodoistClientWithComplete tracks CompleteTask calls
-type mockTodoistClientWithComplete struct {
- mockTodoistClient
- completedTaskIDs []string
- completeErr error
-}
-
-func (m *mockTodoistClientWithComplete) CompleteTask(ctx context.Context, taskID string) error {
- if m.completeErr != nil {
- return m.completeErr
- }
- m.completedTaskIDs = append(m.completedTaskIDs, taskID)
- return nil
-}
-
// mockTrelloClientWithUpdate tracks UpdateCard calls
type mockTrelloClientWithUpdate struct {
mockTrelloClient
@@ -427,138 +294,6 @@ func (m *mockTrelloClientWithUpdate) UpdateCard(ctx context.Context, cardID stri
return nil
}
-// TestHandleCompleteAtom_Todoist tests completing a Todoist task
-func TestHandleCompleteAtom_Todoist(t *testing.T) {
- db, cleanup := setupTestDB(t)
- defer cleanup()
-
- // Save a task to the cache
- tasks := []models.Task{
- {
- ID: "task123",
- Content: "Test task",
- Completed: false,
- Labels: []string{},
- CreatedAt: time.Now(),
- },
- }
- if err := db.SaveTasks(tasks); err != nil {
- t.Fatalf("Failed to save test task: %v", err)
- }
-
- // Verify task exists in cache
- cachedTasks, _ := db.GetTasks()
- if len(cachedTasks) != 1 {
- t.Fatalf("Expected 1 task in cache, got %d", len(cachedTasks))
- }
-
- // Create handler with mock client
- mockTodoist := &mockTodoistClientWithComplete{}
- h := &Handler{
- store: db,
- todoistClient: mockTodoist,
- config: &config.Config{},
- renderer: newTestRenderer(),
- }
-
- // Create request
- req := httptest.NewRequest("POST", "/complete-atom", nil)
- req.Form = map[string][]string{
- "id": {"task123"},
- "source": {"todoist"},
- }
- w := httptest.NewRecorder()
-
- // Execute handler
- h.HandleCompleteAtom(w, req)
-
- // Check response
- if w.Code != http.StatusOK {
- t.Errorf("Expected status 200, got %d", w.Code)
- }
-
- // Verify response body contains rendered completed-atom template
- body := w.Body.String()
- if !strings.Contains(body, "rendered:completed-atom") {
- t.Errorf("Expected response body to contain 'rendered:completed-atom', got %q", body)
- }
-
- // Verify Content-Type header
- ct := w.Header().Get("Content-Type")
- if ct != "text/html; charset=utf-8" {
- t.Errorf("Expected Content-Type 'text/html; charset=utf-8', got %q", ct)
- }
-
- // Verify CompleteTask was called on the API
- if len(mockTodoist.completedTaskIDs) != 1 || mockTodoist.completedTaskIDs[0] != "task123" {
- t.Errorf("Expected CompleteTask to be called with 'task123', got %v", mockTodoist.completedTaskIDs)
- }
-
- // Verify task was deleted from cache
- cachedTasks, _ = db.GetTasks()
- if len(cachedTasks) != 0 {
- t.Errorf("Expected task to be deleted from cache, but found %d tasks", len(cachedTasks))
- }
-}
-
-func TestHandleCompleteAtom_RendersCorrectTemplateData(t *testing.T) {
- db, cleanup := setupTestDB(t)
- defer cleanup()
-
- // Seed a task with known title
- tasks := []models.Task{
- {ID: "task-data-1", Content: "Buy groceries", Labels: []string{}, CreatedAt: time.Now()},
- }
- if err := db.SaveTasks(tasks); err != nil {
- t.Fatalf("Failed to seed: %v", err)
- }
-
- renderer := NewMockRenderer()
- h := &Handler{
- store: db,
- todoistClient: &mockTodoistClientWithComplete{},
- config: &config.Config{},
- renderer: renderer,
- }
-
- req := httptest.NewRequest("POST", "/complete-atom", nil)
- req.Form = map[string][]string{"id": {"task-data-1"}, "source": {"todoist"}}
- w := httptest.NewRecorder()
-
- h.HandleCompleteAtom(w, req)
-
- // Verify renderer was called with "completed-atom" and correct data
- if len(renderer.Calls) != 1 {
- t.Fatalf("Expected 1 render call, got %d", len(renderer.Calls))
- }
- call := renderer.Calls[0]
- if call.Name != "completed-atom" {
- t.Errorf("Expected template 'completed-atom', got %q", call.Name)
- }
-
- // The data should contain the task ID, source, and title
- type atomData struct {
- ID string
- Source string
- Title string
- }
- // Use JSON round-trip to extract fields from the anonymous struct
- jsonBytes, _ := json.Marshal(call.Data)
- var got atomData
- if err := json.Unmarshal(jsonBytes, &got); err != nil {
- t.Fatalf("Failed to unmarshal render data: %v", err)
- }
- if got.ID != "task-data-1" {
- t.Errorf("Expected data.ID 'task-data-1', got %q", got.ID)
- }
- if got.Source != "todoist" {
- t.Errorf("Expected data.Source 'todoist', got %q", got.Source)
- }
- if got.Title != "Buy groceries" {
- t.Errorf("Expected data.Title 'Buy groceries', got %q", got.Title)
- }
-}
-
// TestHandleCompleteAtom_Trello tests completing a Trello card
func TestHandleCompleteAtom_Trello(t *testing.T) {
db, cleanup := setupTestDB(t)
@@ -641,7 +376,7 @@ func TestHandleCompleteAtom_MissingParams(t *testing.T) {
id string
source string
}{
- {"missing id", "", "todoist"},
+ {"missing id", "", "doot"},
{"missing source", "task123", ""},
{"both missing", "", ""},
}
@@ -688,56 +423,6 @@ func TestHandleCompleteAtom_UnknownSource(t *testing.T) {
}
}
-// TestHandleCompleteAtom_APIError tests that API errors are handled correctly
-func TestHandleCompleteAtom_APIError(t *testing.T) {
- db, cleanup := setupTestDB(t)
- defer cleanup()
-
- // Save a task to the cache
- tasks := []models.Task{
- {
- ID: "task123",
- Content: "Test task",
- Completed: false,
- Labels: []string{},
- CreatedAt: time.Now(),
- },
- }
- if err := db.SaveTasks(tasks); err != nil {
- t.Fatalf("Failed to save test task: %v", err)
- }
-
- // Create handler with mock client that returns an error
- mockTodoist := &mockTodoistClientWithComplete{
- completeErr: context.DeadlineExceeded,
- }
- h := &Handler{
- store: db,
- todoistClient: mockTodoist,
- config: &config.Config{},
- }
-
- req := httptest.NewRequest("POST", "/complete-atom", nil)
- req.Form = map[string][]string{
- "id": {"task123"},
- "source": {"todoist"},
- }
- w := httptest.NewRecorder()
-
- h.HandleCompleteAtom(w, req)
-
- // Should return 500 on API error
- if w.Code != http.StatusInternalServerError {
- t.Errorf("Expected status 500 on API error, got %d", w.Code)
- }
-
- // Verify task was NOT deleted from cache (rollback behavior)
- cachedTasks, _ := db.GetTasks()
- if len(cachedTasks) != 1 {
- t.Errorf("Task should NOT be deleted from cache on API error, found %d tasks", len(cachedTasks))
- }
-}
-
// TestHandleShoppingModeComplete tests completing (removing) shopping items
func TestHandleShoppingModeComplete(t *testing.T) {
db, cleanup := setupTestDB(t)
@@ -1354,12 +1039,6 @@ func TestHandleTabTasks(t *testing.T) {
config: &config.Config{CacheTTLMinutes: 5},
}
- // Add some tasks
- tasks := []models.Task{
- {ID: "1", Content: "Task 1", Labels: []string{}, CreatedAt: time.Now()},
- }
- _ = db.SaveTasks(tasks)
-
req := httptest.NewRequest("GET", "/tabs/tasks", nil)
w := httptest.NewRecorder()
@@ -1599,69 +1278,6 @@ func TestHTMLString(t *testing.T) {
// Uncomplete Atom Tests
// =============================================================================
-// mockTodoistClientWithReopen tracks ReopenTask calls
-type mockTodoistClientWithReopen struct {
- mockTodoistClient
- reopenedTaskIDs []string
- reopenErr error
-}
-
-func (m *mockTodoistClientWithReopen) ReopenTask(ctx context.Context, taskID string) error {
- if m.reopenErr != nil {
- return m.reopenErr
- }
- m.reopenedTaskIDs = append(m.reopenedTaskIDs, taskID)
- return nil
-}
-
-func TestHandleUncompleteAtom_Todoist(t *testing.T) {
- db, cleanup := setupTestDB(t)
- defer cleanup()
-
- mockTodoist := &mockTodoistClientWithReopen{}
- h := &Handler{
- store: db,
- todoistClient: mockTodoist,
- config: &config.Config{},
- renderer: newTestRenderer(),
- }
-
- req := httptest.NewRequest("POST", "/uncomplete-atom", nil)
- req.Form = map[string][]string{
- "id": {"task123"},
- "source": {"todoist"},
- }
- w := httptest.NewRecorder()
-
- h.HandleUncompleteAtom(w, req)
-
- if w.Code != http.StatusOK {
- t.Errorf("Expected status 200, got %d", w.Code)
- }
-
- // Verify HX-Reswap header set to "none"
- reswap := w.Header().Get("HX-Reswap")
- if reswap != "none" {
- t.Errorf("Expected HX-Reswap header 'none', got %q", reswap)
- }
-
- // Verify HX-Trigger header set to "refresh-tasks"
- trigger := w.Header().Get("HX-Trigger")
- if trigger != "refresh-tasks" {
- t.Errorf("Expected HX-Trigger header 'refresh-tasks', got %q", trigger)
- }
-
- // Verify response body is empty (no template rendered)
- body := w.Body.String()
- if body != "" {
- t.Errorf("Expected empty response body for uncomplete, got %q", body)
- }
-
- if len(mockTodoist.reopenedTaskIDs) != 1 || mockTodoist.reopenedTaskIDs[0] != "task123" {
- t.Errorf("Expected ReopenTask to be called with 'task123', got %v", mockTodoist.reopenedTaskIDs)
- }
-}
-
// =============================================================================
// Settings Handler Tests
// =============================================================================
@@ -1769,16 +1385,6 @@ func TestHandleGetSourceOptions_MissingSource(t *testing.T) {
}
}
-// mockTodoistClientWithProjects returns mock projects
-type mockTodoistClientWithProjects struct {
- mockTodoistClient
- projects []models.Project
-}
-
-func (m *mockTodoistClientWithProjects) GetProjects(ctx context.Context) ([]models.Project, error) {
- return m.projects, nil
-}
-
// mockTrelloClientWithBoards returns mock boards
type mockTrelloClientWithBoards struct {
mockTrelloClient
@@ -1788,14 +1394,6 @@ func TestHandleSyncSources(t *testing.T) {
h, cleanup := setupTestHandler(t)
defer cleanup()
- // Seed tasks table with project info (projects now come from store, not REST API)
- tasks := []models.Task{
- {ID: "t1", Content: "Task 1", ProjectID: "proj1", ProjectName: "Project 1", Labels: []string{}},
- {ID: "t2", Content: "Task 2", ProjectID: "proj2", ProjectName: "Project 2", Labels: []string{}},
- }
- _ = h.store.SaveTasks(tasks)
-
- mockTodoist := &mockTodoistClient{}
mockTrello := &mockTrelloClientWithBoards{
mockTrelloClient: mockTrelloClient{
boards: []models.Board{
@@ -1804,7 +1402,6 @@ func TestHandleSyncSources(t *testing.T) {
},
}
- h.todoistClient = mockTodoist
h.trelloClient = mockTrello
req := httptest.NewRequest("POST", "/settings/sync", nil)
@@ -1816,10 +1413,10 @@ func TestHandleSyncSources(t *testing.T) {
t.Errorf("Expected status 200, got %d", w.Code)
}
- // Verify configs were synced from store (not REST API)
- configs, _ := h.store.GetSourceConfigsBySource("todoist")
- if len(configs) != 2 {
- t.Errorf("Expected 2 todoist configs, got %d", len(configs))
+ // Verify Trello configs were synced
+ configs, _ := h.store.GetSourceConfigsBySource("trello")
+ if len(configs) != 1 {
+ t.Errorf("Expected 1 trello config, got %d", len(configs))
}
}
@@ -1921,14 +1518,6 @@ func TestHandleTimeline_RendersDataToTemplate(t *testing.T) {
h, cleanup := setupTestHandler(t)
defer cleanup()
- // Seed tasks with due dates in range
- now := config.Now()
- todayNoon := time.Date(now.Year(), now.Month(), now.Day(), 12, 0, 0, 0, now.Location())
- tasks := []models.Task{
- {ID: "t1", Content: "Today task", DueDate: &todayNoon, Labels: []string{}, CreatedAt: now},
- }
- _ = h.store.SaveTasks(tasks)
-
req := httptest.NewRequest("GET", "/tabs/timeline", nil)
w := httptest.NewRecorder()
@@ -1949,14 +1538,10 @@ func TestHandleTimeline_RendersDataToTemplate(t *testing.T) {
t.Errorf("Expected template 'timeline-tab', got '%s'", lastCall.Name)
}
- data, ok := lastCall.Data.(TimelineData)
+ _, ok := lastCall.Data.(TimelineData)
if !ok {
t.Fatalf("Expected TimelineData, got %T", lastCall.Data)
}
-
- if len(data.TodayItems) == 0 {
- t.Error("Expected TodayItems to contain at least one item, got 0")
- }
}
// assertTemplateContains reads a template file and asserts it contains the expected string.
@@ -1979,7 +1564,6 @@ func TestHandleDashboard_ContainsSettingsLink(t *testing.T) {
db, cleanup := setupTestDB(t)
defer cleanup()
- mockTodoist := &mockTodoistClient{}
mockTrello := &mockTrelloClient{}
// Use a renderer that outputs actual content so we can check for settings link
@@ -1994,11 +1578,10 @@ func TestHandleDashboard_ContainsSettingsLink(t *testing.T) {
}
h := &Handler{
- store: db,
- todoistClient: mockTodoist,
- trelloClient: mockTrello,
- config: &config.Config{CacheTTLMinutes: 5},
- renderer: renderer,
+ store: db,
+ trelloClient: mockTrello,
+ config: &config.Config{CacheTTLMinutes: 5},
+ renderer: renderer,
}
req := httptest.NewRequest("GET", "/", nil)
@@ -2112,17 +1695,15 @@ func TestHandleDashboard_PassesBuildVersion(t *testing.T) {
db, cleanup := setupTestDB(t)
defer cleanup()
- mockTodoist := &mockTodoistClient{}
mockTrello := &mockTrelloClient{}
renderer := NewMockRenderer()
h := &Handler{
- store: db,
- todoistClient: mockTodoist,
- trelloClient: mockTrello,
- config: &config.Config{CacheTTLMinutes: 5},
- renderer: renderer,
- BuildVersion: "abc123",
+ store: db,
+ trelloClient: mockTrello,
+ config: &config.Config{CacheTTLMinutes: 5},
+ renderer: renderer,
+ BuildVersion: "abc123",
}
req := httptest.NewRequest("GET", "/", nil)
@@ -2210,123 +1791,20 @@ func TestHandleTimeline_InvalidParams(t *testing.T) {
}
}
-func TestFetchTasks_FetchesAndSavesTasks(t *testing.T) {
- h, cleanup := setupTestHandler(t)
- defer cleanup()
-
- h.todoistClient = &mockTodoistClient{
- tasks: []models.Task{
- {ID: "t1", Content: "Task one", Priority: 1},
- {ID: "t2", Content: "Task two", Priority: 2},
- },
- }
-
- tasks, err := h.fetchTasks(context.Background(), false)
- if err != nil {
- t.Fatalf("fetchTasks returned error: %v", err)
- }
- if len(tasks) != 2 {
- t.Fatalf("Expected 2 tasks, got %d", len(tasks))
- }
-
- // Verify tasks are persisted in the store
- stored, err := h.store.GetTasks()
- if err != nil {
- t.Fatalf("Failed to get tasks from store: %v", err)
- }
- if len(stored) != 2 {
- t.Errorf("Expected 2 stored tasks, got %d", len(stored))
- }
-}
-
-func TestFetchTasks_ReturnsCachedTasksWhenValid(t *testing.T) {
- h, cleanup := setupTestHandler(t)
- defer cleanup()
-
- // Seed cache with tasks and mark it valid
- cached := []models.Task{{ID: "cached-1", Content: "Cached task"}}
- if err := h.store.SaveTasks(cached); err != nil {
- t.Fatalf("Failed to seed tasks: %v", err)
- }
- if err := h.store.UpdateCacheMetadata(store.CacheKeyTodoistTasks, 60); err != nil {
- t.Fatalf("Failed to set cache metadata: %v", err)
- }
-
- // API would return different tasks — should not be called
- h.todoistClient = &mockTodoistClient{
- tasks: []models.Task{{ID: "api-1", Content: "API task"}},
- }
-
- tasks, err := h.fetchTasks(context.Background(), false)
- if err != nil {
- t.Fatalf("fetchTasks returned error: %v", err)
- }
- if len(tasks) != 1 || tasks[0].ID != "cached-1" {
- t.Errorf("Expected cached task, got %+v", tasks)
- }
-}
-
-func TestFetchTasks_ForceRefresh_BypassesCache(t *testing.T) {
- h, cleanup := setupTestHandler(t)
- defer cleanup()
-
- // Seed cache and mark it valid
- if err := h.store.SaveTasks([]models.Task{{ID: "old-1", Content: "Old"}}); err != nil {
- t.Fatalf("Failed to seed tasks: %v", err)
- }
- if err := h.store.UpdateCacheMetadata(store.CacheKeyTodoistTasks, 60); err != nil {
- t.Fatalf("Failed to set cache metadata: %v", err)
- }
-
- h.todoistClient = &mockTodoistClient{
- tasks: []models.Task{{ID: "fresh-1", Content: "Fresh"}},
- }
-
- tasks, err := h.fetchTasks(context.Background(), true)
- if err != nil {
- t.Fatalf("fetchTasks returned error: %v", err)
- }
- if len(tasks) != 1 || tasks[0].ID != "fresh-1" {
- t.Errorf("Expected fresh task from API, got %+v", tasks)
- }
-}
-
-func TestFetchTasks_FallsBackToCacheOnAPIError(t *testing.T) {
- h, cleanup := setupTestHandler(t)
- defer cleanup()
-
- // Seed stale cache
- if err := h.store.SaveTasks([]models.Task{{ID: "stale-1", Content: "Stale"}}); err != nil {
- t.Fatalf("Failed to seed tasks: %v", err)
- }
-
- h.todoistClient = &mockTodoistClient{err: fmt.Errorf("API error")}
-
- tasks, err := h.fetchTasks(context.Background(), false)
- if err != nil {
- t.Fatalf("Expected fallback to cache, got error: %v", err)
- }
- if len(tasks) != 1 || tasks[0].ID != "stale-1" {
- t.Errorf("Expected stale cached task as fallback, got %+v", tasks)
- }
-}
-
// =============================================================================
-// Clear Cache + GetProjects removal tests
+// Clear Cache tests
// =============================================================================
// TestHandleClearCache verifies that POST /settings/clear-cache invalidates
-// all cache keys, clears the todoist sync token, and returns 200 with HX-Trigger.
+// all cache keys and returns 200 with HX-Trigger.
func TestHandleClearCache(t *testing.T) {
h, cleanup := setupTestHandler(t)
defer cleanup()
- // Seed cache metadata and sync token so we can verify they get cleared
- _ = h.store.UpdateCacheMetadata("todoist_tasks", 5)
+ // Seed cache metadata so we can verify it gets cleared
_ = h.store.UpdateCacheMetadata("trello_boards", 5)
_ = h.store.UpdateCacheMetadata("plantoeat_meals", 5)
_ = h.store.UpdateCacheMetadata("google_calendar", 5)
- _ = h.store.SetSyncToken("todoist", "some-sync-token")
req := httptest.NewRequest("POST", "/settings/clear-cache", nil)
w := httptest.NewRecorder()
@@ -2343,62 +1821,20 @@ func TestHandleClearCache(t *testing.T) {
}
// Verify all cache keys invalidated
- for _, key := range []string{"todoist_tasks", "trello_boards", "plantoeat_meals", "google_calendar"} {
+ for _, key := range []string{"trello_boards", "plantoeat_meals", "google_calendar"} {
valid, _ := h.store.IsCacheValid(key)
if valid {
t.Errorf("Cache key %q should be invalidated but is still valid", key)
}
}
-
- // Verify sync token cleared
- token, _ := h.store.GetSyncToken("todoist")
- if token != "" {
- t.Errorf("Expected empty sync token after clear cache, got %q", token)
- }
}
-// TestGetProjectsFromTasks verifies that distinct project pairs are extracted
-// from the tasks table.
-func TestGetProjectsFromTasks(t *testing.T) {
- h, cleanup := setupTestHandler(t)
- defer cleanup()
-
- // Seed tasks with different projects (including duplicates and empty)
- tasks := []models.Task{
- {ID: "1", Content: "Task 1", ProjectID: "p1", ProjectName: "Inbox", Labels: []string{}},
- {ID: "2", Content: "Task 2", ProjectID: "p1", ProjectName: "Inbox", Labels: []string{}},
- {ID: "3", Content: "Task 3", ProjectID: "p2", ProjectName: "Work", Labels: []string{}},
- {ID: "4", Content: "Task 4", ProjectID: "", ProjectName: "", Labels: []string{}},
- }
- if err := h.store.SaveTasks(tasks); err != nil {
- t.Fatalf("Failed to save tasks: %v", err)
- }
-
- projects, err := h.store.GetProjectsFromTasks()
- if err != nil {
- t.Fatalf("GetProjectsFromTasks returned error: %v", err)
- }
-
- if len(projects) != 2 {
- t.Fatalf("Expected 2 distinct projects, got %d: %v", len(projects), projects)
- }
-
- // Verify both projects are present
- found := map[string]bool{}
- for _, p := range projects {
- found[p.ID] = true
- }
- if !found["p1"] || !found["p2"] {
- t.Errorf("Expected projects p1 and p2, got %v", projects)
- }
-}
-
-// TestInvalidateAllCaches verifies the convenience method clears all 4 keys.
+// TestInvalidateAllCaches verifies the convenience method clears all cache keys.
func TestInvalidateAllCaches(t *testing.T) {
h, cleanup := setupTestHandler(t)
defer cleanup()
- keys := []string{"todoist_tasks", "trello_boards", "plantoeat_meals", "google_calendar"}
+ keys := []string{"trello_boards", "plantoeat_meals", "google_calendar"}
for _, key := range keys {
_ = h.store.UpdateCacheMetadata(key, 5)
}
@@ -2423,116 +1859,6 @@ func TestSettingsTemplate_HasClearCacheButton(t *testing.T) {
"settings.html must contain a clear-cache button/endpoint")
}
-// TestAggregateData_DoesNotCallGetProjects verifies that aggregateData no longer
-// calls the deprecated GetProjects REST API.
-func TestAggregateData_DoesNotCallGetProjects(t *testing.T) {
- h, cleanup := setupTestHandler(t)
- defer cleanup()
-
- getProjectsCalled := false
- mock := &mockTodoistClientTracksGetProjects{
- mockTodoistClient: mockTodoistClient{
- tasks: []models.Task{
- {ID: "1", Content: "Test", ProjectID: "p1", ProjectName: "Inbox", Labels: []string{}},
- },
- },
- onGetProjects: func() { getProjectsCalled = true },
- }
- h.todoistClient = mock
- h.trelloClient = &mockTrelloClient{boards: []models.Board{}}
-
- _, err := h.aggregateData(context.Background(), false)
- if err != nil {
- t.Fatalf("aggregateData returned error: %v", err)
- }
-
- if getProjectsCalled {
- t.Error("aggregateData should NOT call GetProjects (deprecated REST API)")
- }
-}
-
-// TestHandleCreateTask_DoesNotCallGetProjects verifies that HandleCreateTask
-// populates projects from the store, not the REST API.
-func TestHandleCreateTask_DoesNotCallGetProjects(t *testing.T) {
- h, cleanup := setupTestHandler(t)
- defer cleanup()
-
- // Seed tasks with project info
- tasks := []models.Task{
- {ID: "1", Content: "Existing", ProjectID: "p1", ProjectName: "Inbox", Labels: []string{}},
- }
- _ = h.store.SaveTasks(tasks)
-
- getProjectsCalled := false
- mock := &mockTodoistClientTracksGetProjects{
- mockTodoistClient: mockTodoistClient{
- tasks: tasks,
- },
- onGetProjects: func() { getProjectsCalled = true },
- }
- h.todoistClient = mock
-
- body := strings.NewReader("content=New+Task&project_id=p1")
- req := httptest.NewRequest("POST", "/tasks", body)
- req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
- w := httptest.NewRecorder()
-
- h.HandleCreateTask(w, req)
-
- if getProjectsCalled {
- t.Error("HandleCreateTask should NOT call GetProjects (deprecated REST API)")
- }
-}
-
-// TestHandleSyncSources_UsesStoreForProjects verifies that HandleSyncSources
-// reads projects from the tasks table instead of the REST API.
-func TestHandleSyncSources_UsesStoreForProjects(t *testing.T) {
- h, cleanup := setupTestHandler(t)
- defer cleanup()
-
- // Seed tasks with project info
- tasks := []models.Task{
- {ID: "1", Content: "Task1", ProjectID: "p1", ProjectName: "Inbox", Labels: []string{}},
- {ID: "2", Content: "Task2", ProjectID: "p2", ProjectName: "Work", Labels: []string{}},
- }
- _ = h.store.SaveTasks(tasks)
-
- getProjectsCalled := false
- mock := &mockTodoistClientTracksGetProjects{
- mockTodoistClient: mockTodoistClient{tasks: tasks},
- onGetProjects: func() { getProjectsCalled = true },
- }
- h.todoistClient = mock
-
- req := httptest.NewRequest("POST", "/settings/sync", nil)
- w := httptest.NewRecorder()
-
- h.HandleSyncSources(w, req)
-
- if getProjectsCalled {
- t.Error("HandleSyncSources should NOT call GetProjects (deprecated REST API)")
- }
-
- // Verify todoist project configs were still synced from store
- configs, _ := h.store.GetSourceConfigsBySource("todoist")
- if len(configs) != 2 {
- t.Errorf("Expected 2 todoist configs from store, got %d", len(configs))
- }
-}
-
-// mockTodoistClientTracksGetProjects wraps mockTodoistClient and tracks GetProjects calls.
-type mockTodoistClientTracksGetProjects struct {
- mockTodoistClient
- onGetProjects func()
-}
-
-func (m *mockTodoistClientTracksGetProjects) GetProjects(ctx context.Context) ([]models.Project, error) {
- if m.onGetProjects != nil {
- m.onGetProjects()
- }
- return []models.Project{}, nil
-}
-
// --- Sync Log Tests ---
// TestStore_AddAndGetSyncLog verifies that sync log entries can be added and retrieved.
@@ -2678,7 +2004,7 @@ func TestHandleGetTaskDetail_RendersTemplate(t *testing.T) {
h, cleanup := setupTestHandler(t)
defer cleanup()
- req := httptest.NewRequest("GET", "/tasks/detail?id=abc&source=todoist", nil)
+ req := httptest.NewRequest("GET", "/tasks/detail?id=abc&source=doot", nil)
w := httptest.NewRecorder()
h.HandleGetTaskDetail(w, req)
@@ -2749,7 +2075,7 @@ func TestHandleSyncSources_AddsLogEntry(t *testing.T) {
// HandleTabPlanning data tests
// =============================================================================
-// TestHandleTabPlanning_HappyPath verifies that tasks, events, and cards are
+// TestHandleTabPlanning_HappyPath verifies that cards are
// placed into the correct planning sections (scheduled/unscheduled/upcoming).
func TestHandleTabPlanning_HappyPath(t *testing.T) {
db, cleanup := setupTestDB(t)
@@ -2757,29 +2083,16 @@ func TestHandleTabPlanning_HappyPath(t *testing.T) {
today := config.Today()
- // Task at 10am today (has time) → goes to Scheduled
- taskWithTime := today.Add(10 * time.Hour)
- // Task at midnight today (no time component) → goes to Unscheduled
- taskNoTime := today
- // Task 2 days from now → goes to Upcoming (today+4 is the upper bound)
- taskUpcoming := today.AddDate(0, 0, 2)
-
- tasks := []models.Task{
- {ID: "t-sched", Content: "Scheduled Task", DueDate: &taskWithTime, Labels: []string{}, CreatedAt: time.Now()},
- {ID: "t-unsched", Content: "Unscheduled Task", DueDate: &taskNoTime, Labels: []string{}, CreatedAt: time.Now()},
- {ID: "t-upcoming", Content: "Upcoming Task", DueDate: &taskUpcoming, Labels: []string{}, CreatedAt: time.Now()},
- }
- if err := db.SaveTasks(tasks); err != nil {
- t.Fatalf("Failed to save tasks: %v", err)
- }
-
// Card at 2pm today → goes to Scheduled
cardWithTime := today.Add(14 * time.Hour)
+ // Card 2 days from now → goes to Upcoming
+ cardUpcoming := today.AddDate(0, 0, 2)
boards := []models.Board{{
ID: "board1",
Name: "Board 1",
Cards: []models.Card{
{ID: "c-sched", Name: "Scheduled Card", DueDate: &cardWithTime, URL: "http://trello.com/c1"},
+ {ID: "c-upcoming", Name: "Upcoming Card", DueDate: &cardUpcoming, URL: "http://trello.com/c2"},
},
}}
if err := db.SaveBoards(boards); err != nil {
@@ -2818,65 +2131,51 @@ func TestHandleTabPlanning_HappyPath(t *testing.T) {
Unscheduled []models.Atom
Upcoming []ScheduledItem
Boards []models.Board
- Projects []models.Project
Today string
})
if !ok {
t.Fatalf("Expected planning data struct, got %T", planningCall.Data)
}
- // t-sched (10am) and c-sched (2pm) should be in Scheduled
- var foundTaskSched, foundCardSched bool
+ // c-sched (2pm) should be in Scheduled
+ var foundCardSched bool
for _, s := range data.Scheduled {
- if s.ID == "t-sched" {
- foundTaskSched = true
- }
if s.ID == "c-sched" {
foundCardSched = true
}
}
- if !foundTaskSched {
- t.Error("Expected t-sched (task with time today) in Scheduled")
- }
if !foundCardSched {
t.Error("Expected c-sched (card with time today) in Scheduled")
}
- // t-unsched (midnight, no time) should be in Unscheduled
- var foundUnsched bool
- for _, u := range data.Unscheduled {
- if u.ID == "t-unsched" {
- foundUnsched = true
- }
- }
- if !foundUnsched {
- t.Error("Expected t-unsched (midnight task) in Unscheduled")
- }
-
- // t-upcoming (2 days out) should be in Upcoming
+ // c-upcoming (2 days out) should be in Upcoming
var foundUpcoming bool
for _, u := range data.Upcoming {
- if u.ID == "t-upcoming" {
+ if u.ID == "c-upcoming" {
foundUpcoming = true
}
}
if !foundUpcoming {
- t.Error("Expected t-upcoming (task in 2 days) in Upcoming")
+ t.Error("Expected c-upcoming (card in 2 days) in Upcoming")
}
}
-// TestHandleTabPlanning_TomorrowBoundary verifies that a task due exactly at
+// TestHandleTabPlanning_TomorrowBoundary verifies that a card due exactly at
// midnight of "tomorrow" is NOT before tomorrow, so it lands in Upcoming.
func TestHandleTabPlanning_TomorrowBoundary(t *testing.T) {
db, cleanup := setupTestDB(t)
defer cleanup()
tomorrow := config.Today().AddDate(0, 0, 1) // midnight tomorrow
- tasks := []models.Task{
- {ID: "t-boundary", Content: "Midnight Tomorrow", DueDate: &tomorrow, Labels: []string{}, CreatedAt: time.Now()},
- }
- if err := db.SaveTasks(tasks); err != nil {
- t.Fatalf("Failed to save tasks: %v", err)
+ boards := []models.Board{{
+ ID: "board1",
+ Name: "Board 1",
+ Cards: []models.Card{
+ {ID: "c-boundary", Name: "Midnight Tomorrow", DueDate: &tomorrow, URL: "http://trello.com/c1"},
+ },
+ }}
+ if err := db.SaveBoards(boards); err != nil {
+ t.Fatalf("Failed to save boards: %v", err)
}
renderer := NewMockRenderer()
@@ -2911,7 +2210,6 @@ func TestHandleTabPlanning_TomorrowBoundary(t *testing.T) {
Unscheduled []models.Atom
Upcoming []ScheduledItem
Boards []models.Board
- Projects []models.Project
Today string
})
if !ok {
@@ -2920,24 +2218,24 @@ func TestHandleTabPlanning_TomorrowBoundary(t *testing.T) {
// midnight-tomorrow: !dueDate.Before(tomorrow) → not in scheduled/unscheduled
for _, s := range data.Scheduled {
- if s.ID == "t-boundary" {
- t.Error("Midnight-tomorrow task must not appear in Scheduled")
+ if s.ID == "c-boundary" {
+ t.Error("Midnight-tomorrow card must not appear in Scheduled")
}
}
for _, u := range data.Unscheduled {
- if u.ID == "t-boundary" {
- t.Error("Midnight-tomorrow task must not appear in Unscheduled")
+ if u.ID == "c-boundary" {
+ t.Error("Midnight-tomorrow card must not appear in Unscheduled")
}
}
// dueDate.Before(in3Days=today+4) → lands in Upcoming
var foundUpcoming bool
for _, u := range data.Upcoming {
- if u.ID == "t-boundary" {
+ if u.ID == "c-boundary" {
foundUpcoming = true
}
}
if !foundUpcoming {
- t.Error("Expected midnight-tomorrow task in Upcoming")
+ t.Error("Expected midnight-tomorrow card in Upcoming")
}
}
diff --git a/internal/handlers/settings.go b/internal/handlers/settings.go
index 32e0215..dc345db 100644
--- a/internal/handlers/settings.go
+++ b/internal/handlers/settings.go
@@ -34,7 +34,7 @@ func (h *Handler) HandleSettingsPage(w http.ResponseWriter, r *http.Request) {
WebAuthnEnabled bool
}{
Configs: bySource,
- Sources: []string{"trello", "todoist", "gcal", "gtasks"},
+ Sources: []string{"trello", "gcal", "gtasks"},
Toggles: toggles,
SyncLog: syncLog,
Agents: agents,
@@ -68,20 +68,6 @@ func (h *Handler) HandleSyncSources(w http.ResponseWriter, r *http.Request) {
}
}
- // Sync Todoist projects from cached tasks (avoids deprecated REST API)
- if projects, err := h.store.GetProjectsFromTasks(); err == nil && len(projects) > 0 {
- var items []models.SourceConfig
- for _, p := range projects {
- items = append(items, models.SourceConfig{
- Source: "todoist",
- ItemType: "project",
- ItemID: p.ID,
- ItemName: p.Name,
- })
- }
- _ = h.store.SyncSourceConfigs("todoist", "project", items)
- }
-
// Sync Google Calendar calendars
if h.googleCalendarClient != nil {
calendars, err := h.googleCalendarClient.GetCalendarList(ctx)
@@ -122,18 +108,13 @@ func (h *Handler) HandleSyncSources(w http.ResponseWriter, r *http.Request) {
h.HandleSettingsPage(w, r)
}
-// HandleClearCache invalidates all caches and clears the Todoist sync token
+// HandleClearCache invalidates all caches
func (h *Handler) HandleClearCache(w http.ResponseWriter, r *http.Request) {
if err := h.store.InvalidateAllCaches(); err != nil {
JSONError(w, http.StatusInternalServerError, "Failed to clear cache", err)
return
}
- if err := h.store.ClearSyncToken("todoist"); err != nil {
- JSONError(w, http.StatusInternalServerError, "Failed to clear sync token", err)
- return
- }
-
_ = h.store.AddSyncLogEntry("cache_clear", "Cache cleared — next load fetches fresh data")
syncLog, _ := h.store.GetRecentSyncLog(20)
@@ -167,8 +148,6 @@ func (h *Handler) HandleToggleSourceConfig(w http.ResponseWriter, r *http.Reques
switch source {
case "trello":
_ = h.store.InvalidateCache("trello_boards")
- case "todoist":
- _ = h.store.InvalidateCache("todoist_tasks")
}
w.Header().Set("Content-Type", "application/json")
diff --git a/internal/handlers/tab_state_test.go b/internal/handlers/tab_state_test.go
index d066966..9b34033 100644
--- a/internal/handlers/tab_state_test.go
+++ b/internal/handlers/tab_state_test.go
@@ -33,10 +33,9 @@ func TestHandleDashboard_TabState(t *testing.T) {
}
h := &Handler{
- store: db,
- renderer: mock,
- todoistClient: &mockTodoistClient{},
- trelloClient: &mockTrelloClient{},
+ store: db,
+ renderer: mock,
+ trelloClient: &mockTrelloClient{},
config: &config.Config{
Port: "8080",
CacheTTLMinutes: 5,
diff --git a/internal/handlers/timeline_logic.go b/internal/handlers/timeline_logic.go
index c9510bf..67ba09d 100644
--- a/internal/handlers/timeline_logic.go
+++ b/internal/handlers/timeline_logic.go
@@ -17,54 +17,7 @@ func BuildTimeline(ctx context.Context, s *store.Store, start, end time.Time) ([
var items []models.TimelineItem
now := config.Now()
- // 1. Fetch Tasks (dated)
- tasks, err := s.GetTasksByDateRange(start, end)
- if err != nil {
- return nil, err
- }
- for _, task := range tasks {
- if task.DueDate == nil {
- continue
- }
- item := models.TimelineItem{
- ID: task.ID,
- Type: models.TimelineItemTypeTask,
- Title: task.Content,
- Time: *task.DueDate,
- Description: task.Description,
- URL: task.URL,
- OriginalItem: task,
- IsCompleted: task.Completed,
- Source: "todoist",
- }
- item.ComputeDaySection(now)
- items = append(items, item)
- }
-
- // 1b. Fetch undated Tasks — place in Today section
- undatedTasks, err := s.GetUndatedTasks()
- if err != nil {
- log.Printf("Warning: failed to fetch undated tasks: %v", err)
- } else {
- for _, task := range undatedTasks {
- item := models.TimelineItem{
- ID: task.ID,
- Type: models.TimelineItemTypeTask,
- Title: task.Content,
- Time: now,
- Description: task.Description,
- URL: task.URL,
- OriginalItem: task,
- IsCompleted: task.Completed,
- Source: "todoist",
- IsAllDay: true,
- }
- item.ComputeDaySection(now)
- items = append(items, item)
- }
- }
-
- // 2. Fetch Meals - combine multiple items for same date+mealType
+ // 1. Fetch Meals - combine multiple items for same date+mealType
meals, err := s.GetMealsByDateRange(start, end)
if err != nil {
return nil, err
diff --git a/internal/handlers/timeline_logic_test.go b/internal/handlers/timeline_logic_test.go
index 03bc291..3678b62 100644
--- a/internal/handlers/timeline_logic_test.go
+++ b/internal/handlers/timeline_logic_test.go
@@ -46,20 +46,6 @@ func setupTestStore(t *testing.T) *store.Store {
}
schema := `
- CREATE TABLE IF NOT EXISTS tasks (
- id TEXT PRIMARY KEY,
- content TEXT NOT NULL,
- description TEXT,
- project_id TEXT,
- project_name TEXT,
- due_date DATETIME,
- priority INTEGER DEFAULT 1,
- completed BOOLEAN DEFAULT FALSE,
- labels TEXT,
- url TEXT,
- created_at DATETIME,
- updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
- );
CREATE TABLE IF NOT EXISTS meals (
id TEXT PRIMARY KEY,
recipe_name TEXT NOT NULL,
@@ -118,15 +104,9 @@ func setupTestStore(t *testing.T) *store.Store {
func TestBuildTimeline(t *testing.T) {
s := setupTestStore(t)
-
+
// Fix a base time: 2023-01-01 08:00:00
baseTime := time.Date(2023, 1, 1, 8, 0, 0, 0, time.UTC)
-
- // Task: 10:00
- taskDate := baseTime.Add(2 * time.Hour)
- _ = s.SaveTasks([]models.Task{
- {ID: "t1", Content: "Task 1", DueDate: &taskDate},
- })
// Meal: Lunch (defaults to 12:00)
mealDate := baseTime // Date part matters
@@ -138,7 +118,7 @@ func TestBuildTimeline(t *testing.T) {
cardDate := baseTime.Add(6 * time.Hour)
_ = s.SaveBoards([]models.Board{
{
- ID: "b1",
+ ID: "b1",
Name: "Board 1",
Cards: []models.Card{
{ID: "c1", Name: "Card 1", DueDate: &cardDate, ListID: "l1"},
@@ -161,27 +141,23 @@ func TestBuildTimeline(t *testing.T) {
t.Fatalf("BuildTimeline failed: %v", err)
}
- if len(items) != 4 {
- t.Errorf("Expected 4 items, got %d", len(items))
+ if len(items) != 3 {
+ t.Errorf("Expected 3 items, got %d", len(items))
}
// Expected Order:
// 1. Event (09:00)
- // 2. Task (10:00)
- // 3. Meal (12:00)
- // 4. Card (14:00)
+ // 2. Meal (12:00)
+ // 3. Card (14:00)
if items[0].Type != models.TimelineItemTypeEvent {
t.Errorf("Expected item 0 to be Event, got %s", items[0].Type)
}
- if items[1].Type != models.TimelineItemTypeTask {
- t.Errorf("Expected item 1 to be Task, got %s", items[1].Type)
+ if items[1].Type != models.TimelineItemTypeMeal {
+ t.Errorf("Expected item 1 to be Meal, got %s", items[1].Type)
}
- if items[2].Type != models.TimelineItemTypeMeal {
- t.Errorf("Expected item 2 to be Meal, got %s", items[2].Type)
- }
- if items[3].Type != models.TimelineItemTypeCard {
- t.Errorf("Expected item 3 to be Card, got %s", items[3].Type)
+ if items[2].Type != models.TimelineItemTypeCard {
+ t.Errorf("Expected item 2 to be Card, got %s", items[2].Type)
}
}
@@ -273,78 +249,6 @@ func TestCalcCalendarBounds(t *testing.T) {
}
}
-func TestBuildTimeline_IncludesOverdueItems(t *testing.T) {
- s := setupTestStore(t)
-
- // Base: "today" is Jan 2, 2023
- today := time.Date(2023, 1, 2, 0, 0, 0, 0, time.UTC)
- yesterday := time.Date(2023, 1, 1, 10, 0, 0, 0, time.UTC) // overdue
- todayNoon := time.Date(2023, 1, 2, 12, 0, 0, 0, time.UTC) // current
-
- // Save a task that is overdue (yesterday) and one that is current (today)
- _ = s.SaveTasks([]models.Task{
- {ID: "overdue1", Content: "Overdue task", DueDate: &yesterday},
- {ID: "current1", Content: "Current task", DueDate: &todayNoon},
- })
-
- // Query range: today through tomorrow
- end := today.AddDate(0, 0, 1)
-
- items, err := BuildTimeline(context.Background(), s, today, end)
- if err != nil {
- t.Fatalf("BuildTimeline failed: %v", err)
- }
-
- // Should include both the overdue task and the current task
- if len(items) < 2 {
- t.Errorf("Expected at least 2 items (overdue + current), got %d", len(items))
- for _, item := range items {
- t.Logf(" item: %s (type=%s, time=%s)", item.Title, item.Type, item.Time)
- }
- }
-
- // Verify overdue task is marked as overdue
- foundOverdue := false
- for _, item := range items {
- if item.ID == "overdue1" {
- foundOverdue = true
- if !item.IsOverdue {
- t.Error("Expected overdue task to be marked IsOverdue=true")
- }
- if item.DaySection != models.DaySectionToday {
- t.Errorf("Expected overdue task in Today section, got %s", item.DaySection)
- }
- }
- }
- if !foundOverdue {
- t.Error("Overdue task was not included in timeline results")
- }
-}
-
-func TestBuildTimeline_ExcludesCompletedOverdue(t *testing.T) {
- s := setupTestStore(t)
-
- yesterday := time.Date(2023, 1, 1, 10, 0, 0, 0, time.UTC)
- today := time.Date(2023, 1, 2, 0, 0, 0, 0, time.UTC)
- end := today.AddDate(0, 0, 1)
-
- // Save a completed overdue task — should NOT appear
- _ = s.SaveTasks([]models.Task{
- {ID: "done1", Content: "Done overdue", DueDate: &yesterday, Completed: true},
- })
-
- items, err := BuildTimeline(context.Background(), s, today, end)
- if err != nil {
- t.Fatalf("BuildTimeline failed: %v", err)
- }
-
- for _, item := range items {
- if item.ID == "done1" {
- t.Error("Completed overdue task should not appear in timeline")
- }
- }
-}
-
func TestBuildTimeline_ReadsCalendarEventsFromStore(t *testing.T) {
s := setupTestStore(t)
@@ -483,36 +387,6 @@ func TestSaveAndGetCalendarEvents(t *testing.T) {
}
}
-func TestBuildTimeline_IncludesUndatedTodoistTasks(t *testing.T) {
- s := setupTestStore(t)
-
- // Save a task with no due date
- _ = s.SaveTasks([]models.Task{
- {ID: "undated1", Content: "Take out recycling"}, // DueDate is nil
- })
-
- start := time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC)
- end := time.Date(2023, 1, 2, 0, 0, 0, 0, time.UTC)
-
- items, err := BuildTimeline(context.Background(), s, start, end)
- if err != nil {
- t.Fatalf("BuildTimeline failed: %v", err)
- }
-
- found := false
- for _, item := range items {
- if item.ID == "undated1" {
- found = true
- if item.DaySection != models.DaySectionToday {
- t.Errorf("Undated task should be in Today section, got %s", item.DaySection)
- }
- }
- }
- if !found {
- t.Error("Undated Todoist task should appear in timeline Today section")
- }
-}
-
func timePtr(t time.Time) *time.Time {
return &t
}
diff --git a/internal/handlers/widget.go b/internal/handlers/widget.go
index ee13dc4..876aaad 100644
--- a/internal/handlers/widget.go
+++ b/internal/handlers/widget.go
@@ -47,7 +47,7 @@ func TimelineItemToWidgetItem(item models.TimelineItem) models.WidgetItem {
// not completable via widget API
default:
wi.Type = "task"
- wi.Completable = item.Source == "todoist" || item.Source == "doot"
+ wi.Completable = item.Source == "doot"
}
// Only populate Start/End for items with a real time (non-all-day, non-zero)
@@ -147,15 +147,6 @@ func (h *Handler) HandleWidgetComplete(w http.ResponseWriter, r *http.Request) {
return
}
_ = h.store.SaveCompletedTask("doot", req.ID, "", nil)
- case "todoist":
- if h.todoistClient == nil {
- http.Error(w, "todoist not configured", http.StatusServiceUnavailable)
- return
- }
- if err := h.todoistClient.CompleteTask(r.Context(), req.ID); err != nil {
- http.Error(w, "failed to complete task", http.StatusInternalServerError)
- return
- }
default:
http.Error(w, "source not completable", http.StatusBadRequest)
return
diff --git a/internal/handlers/widget_test.go b/internal/handlers/widget_test.go
index 5b054a0..f56fe07 100644
--- a/internal/handlers/widget_test.go
+++ b/internal/handlers/widget_test.go
@@ -61,11 +61,11 @@ func TestTimelineItemToWidgetItem_Task(t *testing.T) {
item := models.TimelineItem{
ID: "abc",
Title: "Write notes",
- Source: "todoist",
+ Source: "doot",
Type: models.TimelineItemTypeTask,
Time: now,
IsAllDay: true,
- URL: "https://todoist.com/task/abc",
+ URL: "https://example.com/task/abc",
}
wi := TimelineItemToWidgetItem(item)
@@ -77,7 +77,7 @@ func TestTimelineItemToWidgetItem_Task(t *testing.T) {
t.Errorf("Type: got %q, want %q", wi.Type, "task")
}
if !wi.Completable {
- t.Error("todoist task should be completable")
+ t.Error("doot task should be completable")
}
if wi.Start != nil {
t.Error("all-day item should have nil Start")
@@ -109,7 +109,7 @@ func TestTimelineItemToWidgetItem_Event(t *testing.T) {
}
}
-func TestHandleWidgetComplete_NonTodoist(t *testing.T) {
+func TestHandleWidgetComplete_NonCompletable(t *testing.T) {
h := &Handler{}
body := `{"id":"x","source":"calendar"}`
req := httptest.NewRequest("POST", "/api/widget/complete", strings.NewReader(body))