summaryrefslogtreecommitdiff
path: root/internal/handlers/handlers.go
diff options
context:
space:
mode:
authorPeter Stone <thepeterstone@gmail.com>2026-03-23 08:13:02 +0000
committerPeter Stone <thepeterstone@gmail.com>2026-03-23 08:13:02 +0000
commitb2d8fc460be3105ac383098e7cdc92171e5026ec (patch)
treecd5ba3f3008e6b3310680d785880f1f32ed090c5 /internal/handlers/handlers.go
parentb0688c8819da1b7fcb4a97b6ec1fa58050e4841e (diff)
feat: unify Google Tasks with main system via caching and integrated UI
- Implement SQLite caching layer for Google Tasks - Integrate Google Tasks into unified Atoms loop (showing in Tasks tab) - Update Planning tab to include cached Google Tasks - Enhance Quick Add form with Todoist project selector - Remove orphaned HandleTasksTab/HandleRefreshTab methods - Update tests to reflect new BuildTimeline signature and data structures
Diffstat (limited to 'internal/handlers/handlers.go')
-rw-r--r--internal/handlers/handlers.go109
1 files changed, 87 insertions, 22 deletions
diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go
index b0fd952..8abd4d7 100644
--- a/internal/handlers/handlers.go
+++ b/internal/handlers/handlers.go
@@ -173,26 +173,6 @@ func (h *Handler) HandleGetShoppingList(w http.ResponseWriter, r *http.Request)
JSONResponse(w, items)
}
-// HandleTasksTab renders the tasks tab content (Trello + Todoist + PlanToEat)
-func (h *Handler) HandleTasksTab(w http.ResponseWriter, r *http.Request) {
- data, err := h.aggregateData(r.Context(), false)
- if err != nil {
- JSONError(w, http.StatusInternalServerError, "Failed to load tasks", err)
- return
- }
- HTMLResponse(w, h.renderer, "tasks-tab", data)
-}
-
-// HandleRefreshTab refreshes and re-renders the specified tab
-func (h *Handler) HandleRefreshTab(w http.ResponseWriter, r *http.Request) {
- data, err := h.aggregateData(r.Context(), true)
- if err != nil {
- JSONError(w, http.StatusInternalServerError, "Failed to refresh", err)
- return
- }
- HTMLResponse(w, h.renderer, "tasks-tab", data)
-}
-
// aggregateData fetches and caches data from all sources concurrently
func (h *Handler) aggregateData(ctx context.Context, forceRefresh bool) (*models.DashboardData, error) {
data := &models.DashboardData{
@@ -267,6 +247,18 @@ func (h *Handler) aggregateData(ctx context.Context, forceRefresh bool) (*models
})
}
+ if h.googleTasksClient != nil {
+ fetch("Google Tasks", func() error {
+ tasks, err := h.fetchGoogleTasks(ctx, forceRefresh)
+ if err == nil {
+ mu.Lock()
+ data.GoogleTasks = tasks
+ mu.Unlock()
+ }
+ return err
+ })
+ }
+
wg.Wait()
// Populate projects from cached tasks (avoids deprecated REST API)
@@ -392,6 +384,22 @@ func (h *Handler) fetchCalendarEvents(ctx context.Context, forceRefresh bool) ([
return fetcher.FetchWithCache(ctx, forceRefresh)
}
+// fetchGoogleTasks fetches Google Tasks from cache or API
+func (h *Handler) fetchGoogleTasks(ctx context.Context, forceRefresh bool) ([]models.GoogleTask, error) {
+ if h.googleTasksClient == nil {
+ return nil, nil
+ }
+ fetcher := &CacheFetcher[models.GoogleTask]{
+ Store: h.store,
+ CacheKey: store.CacheKeyGoogleTasks,
+ TTLMinutes: h.config.CacheTTLMinutes,
+ Fetch: func(ctx context.Context) ([]models.GoogleTask, error) { return h.googleTasksClient.GetTasks(ctx) },
+ GetFromCache: h.store.GetGoogleTasks,
+ SaveToCache: h.store.SaveGoogleTasks,
+ }
+ return fetcher.FetchWithCache(ctx, forceRefresh)
+}
+
// fetchBoards fetches Trello boards from cache or API
func (h *Handler) fetchBoards(ctx context.Context, forceRefresh bool) ([]models.Board, error) {
fetcher := &CacheFetcher[models.Board]{
@@ -684,7 +692,8 @@ func (h *Handler) HandleUnifiedAdd(w http.ResponseWriter, r *http.Request) {
switch source {
case "todoist":
- if _, err := h.todoistClient.CreateTask(ctx, title, "", dueDate, 1); err != nil {
+ 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
}
@@ -805,7 +814,7 @@ func (h *Handler) HandleUpdateTask(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}
-// HandleTabTasks renders the unified Tasks tab (Todoist + Trello cards with due dates + Bugs)
+// 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)
if err != nil {
@@ -816,15 +825,19 @@ 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"),
}
@@ -839,6 +852,7 @@ func (h *Handler) HandleTabPlanning(w http.ResponseWriter, r *http.Request) {
boards, _ := h.store.GetBoards()
tasks, _ := h.store.GetTasks()
+ gTasks, _ := h.store.GetGoogleTasks()
events, _ := h.fetchCalendarEvents(r.Context(), false)
@@ -941,21 +955,72 @@ func (h *Handler) HandleTabPlanning(w http.ResponseWriter, r *http.Request) {
}
}
+ for _, gTask := range gTasks {
+ if gTask.Completed {
+ continue
+ }
+
+ if gTask.DueDate == nil {
+ atom := models.GoogleTaskToAtom(gTask)
+ atom.ComputeUIFields()
+ unscheduled = append(unscheduled, atom)
+ continue
+ }
+
+ dueDate := *gTask.DueDate
+ // Google Tasks usually don't have times, but if they did we'd handle them here
+ hasTime := dueDate.Hour() != 0 || dueDate.Minute() != 0
+
+ if dueDate.Before(tomorrow) {
+ if hasTime {
+ scheduled = append(scheduled, ScheduledItem{
+ Type: "task",
+ ID: gTask.ID,
+ Title: gTask.Title,
+ Start: dueDate,
+ URL: gTask.URL,
+ Source: "gtasks",
+ SourceIcon: "🔵",
+ Priority: 2,
+ })
+ } else {
+ atom := models.GoogleTaskToAtom(gTask)
+ atom.ComputeUIFields()
+ unscheduled = append(unscheduled, atom)
+ }
+ } else if dueDate.Before(in3Days) {
+ upcoming = append(upcoming, ScheduledItem{
+ Type: "task",
+ ID: gTask.ID,
+ Title: gTask.Title,
+ Start: dueDate,
+ URL: gTask.URL,
+ Source: "gtasks",
+ SourceIcon: "🔵",
+ Priority: 2,
+ })
+ }
+ }
+
sort.Slice(scheduled, func(i, j int) bool { return scheduled[i].Start.Before(scheduled[j].Start) })
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"),
}