summaryrefslogtreecommitdiff
path: root/internal/handlers/handlers.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/handlers/handlers.go')
-rw-r--r--internal/handlers/handlers.go360
1 files changed, 360 insertions, 0 deletions
diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go
new file mode 100644
index 0000000..6872ba7
--- /dev/null
+++ b/internal/handlers/handlers.go
@@ -0,0 +1,360 @@
+package handlers
+
+import (
+ "context"
+ "encoding/json"
+ "html/template"
+ "log"
+ "net/http"
+ "sync"
+ "time"
+
+ "task-dashboard/internal/api"
+ "task-dashboard/internal/config"
+ "task-dashboard/internal/models"
+ "task-dashboard/internal/store"
+)
+
+// Handler holds dependencies for HTTP handlers
+type Handler struct {
+ store *store.Store
+ todoistClient api.TodoistAPI
+ trelloClient api.TrelloAPI
+ obsidianClient api.ObsidianAPI
+ planToEatClient api.PlanToEatAPI
+ config *config.Config
+ templates *template.Template
+}
+
+// New creates a new Handler instance
+func New(store *store.Store, todoist api.TodoistAPI, trello api.TrelloAPI, obsidian api.ObsidianAPI, planToEat api.PlanToEatAPI, cfg *config.Config) *Handler {
+ // Parse templates
+ tmpl, err := template.ParseGlob("web/templates/*.html")
+ if err != nil {
+ log.Printf("Warning: failed to parse templates: %v", err)
+ }
+
+ return &Handler{
+ store: store,
+ todoistClient: todoist,
+ trelloClient: trello,
+ obsidianClient: obsidian,
+ planToEatClient: planToEat,
+ config: cfg,
+ templates: tmpl,
+ }
+}
+
+// HandleDashboard renders the main dashboard view
+func (h *Handler) HandleDashboard(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+
+ // Aggregate data from all sources
+ data, err := h.aggregateData(ctx, false)
+ if err != nil {
+ http.Error(w, "Failed to load dashboard data", http.StatusInternalServerError)
+ log.Printf("Error aggregating data: %v", err)
+ return
+ }
+
+ // Render template
+ if h.templates == nil {
+ http.Error(w, "Templates not loaded", http.StatusInternalServerError)
+ return
+ }
+
+ if err := h.templates.ExecuteTemplate(w, "index.html", data); err != nil {
+ http.Error(w, "Failed to render template", http.StatusInternalServerError)
+ log.Printf("Error rendering template: %v", err)
+ }
+}
+
+// HandleRefresh forces a refresh of all data
+func (h *Handler) HandleRefresh(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+
+ // Force refresh by passing true
+ data, err := h.aggregateData(ctx, true)
+ if err != nil {
+ http.Error(w, "Failed to refresh data", http.StatusInternalServerError)
+ log.Printf("Error refreshing data: %v", err)
+ return
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(data)
+}
+
+// HandleGetTasks returns tasks as JSON
+func (h *Handler) HandleGetTasks(w http.ResponseWriter, r *http.Request) {
+ tasks, err := h.store.GetTasks()
+ if err != nil {
+ http.Error(w, "Failed to get tasks", http.StatusInternalServerError)
+ return
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(tasks)
+}
+
+// HandleGetNotes returns notes as JSON
+func (h *Handler) HandleGetNotes(w http.ResponseWriter, r *http.Request) {
+ notes, err := h.store.GetNotes(20)
+ if err != nil {
+ http.Error(w, "Failed to get notes", http.StatusInternalServerError)
+ return
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(notes)
+}
+
+// HandleGetMeals returns meals as JSON
+func (h *Handler) HandleGetMeals(w http.ResponseWriter, r *http.Request) {
+ startDate := time.Now()
+ endDate := startDate.AddDate(0, 0, 7)
+
+ meals, err := h.store.GetMeals(startDate, endDate)
+ if err != nil {
+ http.Error(w, "Failed to get meals", http.StatusInternalServerError)
+ return
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(meals)
+}
+
+// HandleGetBoards returns Trello boards with cards as JSON
+func (h *Handler) HandleGetBoards(w http.ResponseWriter, r *http.Request) {
+ boards, err := h.store.GetBoards()
+ if err != nil {
+ http.Error(w, "Failed to get boards", http.StatusInternalServerError)
+ return
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(boards)
+}
+
+// aggregateData fetches and caches data from all sources
+func (h *Handler) aggregateData(ctx context.Context, forceRefresh bool) (*models.DashboardData, error) {
+ data := &models.DashboardData{
+ LastUpdated: time.Now(),
+ Errors: make([]string, 0),
+ }
+
+ var wg sync.WaitGroup
+ var mu sync.Mutex
+
+ // Fetch Trello boards (PRIORITY - most important)
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ boards, err := h.fetchBoards(ctx, forceRefresh)
+ mu.Lock()
+ defer mu.Unlock()
+ if err != nil {
+ data.Errors = append(data.Errors, "Trello: "+err.Error())
+ } else {
+ data.Boards = boards
+ }
+ }()
+
+ // Fetch Todoist tasks
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ tasks, err := h.fetchTasks(ctx, forceRefresh)
+ mu.Lock()
+ defer mu.Unlock()
+ if err != nil {
+ data.Errors = append(data.Errors, "Todoist: "+err.Error())
+ } else {
+ data.Tasks = tasks
+ }
+ }()
+
+ // Fetch Obsidian notes (if configured)
+ if h.obsidianClient != nil {
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ notes, err := h.fetchNotes(ctx, forceRefresh)
+ mu.Lock()
+ defer mu.Unlock()
+ if err != nil {
+ data.Errors = append(data.Errors, "Obsidian: "+err.Error())
+ } else {
+ data.Notes = notes
+ }
+ }()
+ }
+
+ // Fetch PlanToEat meals (if configured)
+ if h.planToEatClient != nil {
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ meals, err := h.fetchMeals(ctx, forceRefresh)
+ mu.Lock()
+ defer mu.Unlock()
+ if err != nil {
+ data.Errors = append(data.Errors, "PlanToEat: "+err.Error())
+ } else {
+ data.Meals = meals
+ }
+ }()
+ }
+
+ wg.Wait()
+
+ return data, nil
+}
+
+// fetchTasks fetches tasks from cache or API
+func (h *Handler) fetchTasks(ctx context.Context, forceRefresh bool) ([]models.Task, error) {
+ cacheKey := "todoist_tasks"
+
+ // Check cache validity
+ if !forceRefresh {
+ valid, err := h.store.IsCacheValid(cacheKey)
+ if err == nil && valid {
+ return h.store.GetTasks()
+ }
+ }
+
+ // Fetch from API
+ tasks, err := h.todoistClient.GetTasks(ctx)
+ if err != nil {
+ // Try to return cached data even if stale
+ cachedTasks, cacheErr := h.store.GetTasks()
+ if cacheErr == nil && len(cachedTasks) > 0 {
+ return cachedTasks, nil
+ }
+ return nil, err
+ }
+
+ // Save to cache
+ if err := h.store.SaveTasks(tasks); err != nil {
+ log.Printf("Failed to save tasks to cache: %v", err)
+ }
+
+ // Update cache metadata
+ if err := h.store.UpdateCacheMetadata(cacheKey, h.config.CacheTTLMinutes); err != nil {
+ log.Printf("Failed to update cache metadata: %v", err)
+ }
+
+ return tasks, nil
+}
+
+// fetchNotes fetches notes from cache or filesystem
+func (h *Handler) fetchNotes(ctx context.Context, forceRefresh bool) ([]models.Note, error) {
+ cacheKey := "obsidian_notes"
+
+ // Check cache validity
+ if !forceRefresh {
+ valid, err := h.store.IsCacheValid(cacheKey)
+ if err == nil && valid {
+ return h.store.GetNotes(20)
+ }
+ }
+
+ // Fetch from filesystem
+ notes, err := h.obsidianClient.GetNotes(ctx, 20)
+ if err != nil {
+ // Try to return cached data even if stale
+ cachedNotes, cacheErr := h.store.GetNotes(20)
+ if cacheErr == nil && len(cachedNotes) > 0 {
+ return cachedNotes, nil
+ }
+ return nil, err
+ }
+
+ // Save to cache
+ if err := h.store.SaveNotes(notes); err != nil {
+ log.Printf("Failed to save notes to cache: %v", err)
+ }
+
+ // Update cache metadata
+ if err := h.store.UpdateCacheMetadata(cacheKey, h.config.CacheTTLMinutes); err != nil {
+ log.Printf("Failed to update cache metadata: %v", err)
+ }
+
+ return notes, nil
+}
+
+// fetchMeals fetches meals from cache or API
+func (h *Handler) fetchMeals(ctx context.Context, forceRefresh bool) ([]models.Meal, error) {
+ cacheKey := "plantoeat_meals"
+
+ // Check cache validity
+ if !forceRefresh {
+ valid, err := h.store.IsCacheValid(cacheKey)
+ if err == nil && valid {
+ startDate := time.Now()
+ endDate := startDate.AddDate(0, 0, 7)
+ return h.store.GetMeals(startDate, endDate)
+ }
+ }
+
+ // Fetch from API
+ meals, err := h.planToEatClient.GetUpcomingMeals(ctx, 7)
+ if err != nil {
+ // Try to return cached data even if stale
+ startDate := time.Now()
+ endDate := startDate.AddDate(0, 0, 7)
+ cachedMeals, cacheErr := h.store.GetMeals(startDate, endDate)
+ if cacheErr == nil && len(cachedMeals) > 0 {
+ return cachedMeals, nil
+ }
+ return nil, err
+ }
+
+ // Save to cache
+ if err := h.store.SaveMeals(meals); err != nil {
+ log.Printf("Failed to save meals to cache: %v", err)
+ }
+
+ // Update cache metadata
+ if err := h.store.UpdateCacheMetadata(cacheKey, h.config.CacheTTLMinutes); err != nil {
+ log.Printf("Failed to update cache metadata: %v", err)
+ }
+
+ return meals, nil
+}
+
+// fetchBoards fetches Trello boards from cache or API
+func (h *Handler) fetchBoards(ctx context.Context, forceRefresh bool) ([]models.Board, error) {
+ cacheKey := "trello_boards"
+
+ // Check cache validity
+ if !forceRefresh {
+ valid, err := h.store.IsCacheValid(cacheKey)
+ if err == nil && valid {
+ return h.store.GetBoards()
+ }
+ }
+
+ // Fetch from API
+ boards, err := h.trelloClient.GetBoardsWithCards(ctx)
+ if err != nil {
+ // Try to return cached data even if stale
+ cachedBoards, cacheErr := h.store.GetBoards()
+ if cacheErr == nil && len(cachedBoards) > 0 {
+ return cachedBoards, nil
+ }
+ return nil, err
+ }
+
+ // Save to cache
+ if err := h.store.SaveBoards(boards); err != nil {
+ log.Printf("Failed to save boards to cache: %v", err)
+ }
+
+ // Update cache metadata
+ if err := h.store.UpdateCacheMetadata(cacheKey, h.config.CacheTTLMinutes); err != nil {
+ log.Printf("Failed to update cache metadata: %v", err)
+ }
+
+ return boards, nil
+}