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 }