diff options
Diffstat (limited to 'internal/handlers/handlers.go')
| -rw-r--r-- | internal/handlers/handlers.go | 360 |
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 +} |
