summaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
authorPeter Stone <thepeterstone@gmail.com>2026-01-22 15:28:06 -1000
committerPeter Stone <thepeterstone@gmail.com>2026-01-22 15:31:50 -1000
commite97a1bc259d3aa91956ec73a522421cdb621ae57 (patch)
treea9f424ef97673c0dd7da8cee6044413e6417b626 /internal
parentdb5deb2330448c75ba6b719e6a397832362b222a (diff)
Add Google Calendar integration
- Add GoogleCalendarClient for fetching upcoming events - Add GoogleCalendarAPI interface and CalendarEvent model - Add config for GOOGLE_CREDENTIALS_FILE and GOOGLE_CALENDAR_ID - Display events in Planning tab with date/time formatting - Update handlers and tests to support optional calendar client Config env vars: - GOOGLE_CREDENTIALS_FILE: Path to service account JSON - GOOGLE_CALENDAR_ID: Calendar ID (defaults to "primary") Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Diffstat (limited to 'internal')
-rw-r--r--internal/api/google_calendar.go62
-rw-r--r--internal/api/interfaces.go12
-rw-r--r--internal/config/config.go13
-rw-r--r--internal/handlers/handlers.go44
-rw-r--r--internal/handlers/heuristic_test.go2
-rw-r--r--internal/handlers/tab_state_test.go2
-rw-r--r--internal/handlers/tabs.go26
-rw-r--r--internal/models/types.go25
8 files changed, 156 insertions, 30 deletions
diff --git a/internal/api/google_calendar.go b/internal/api/google_calendar.go
new file mode 100644
index 0000000..836d98c
--- /dev/null
+++ b/internal/api/google_calendar.go
@@ -0,0 +1,62 @@
+package api
+
+import (
+ "context"
+ "fmt"
+ "time"
+
+ "task-dashboard/internal/models"
+
+ "google.golang.org/api/calendar/v3"
+ "google.golang.org/api/option"
+)
+
+type GoogleCalendarClient struct {
+ srv *calendar.Service
+ calendarID string
+}
+
+func NewGoogleCalendarClient(ctx context.Context, credentialsFile, calendarID string) (*GoogleCalendarClient, error) {
+ srv, err := calendar.NewService(ctx, option.WithCredentialsFile(credentialsFile))
+ if err != nil {
+ return nil, fmt.Errorf("unable to retrieve Calendar client: %v", err)
+ }
+
+ return &GoogleCalendarClient{
+ srv: srv,
+ calendarID: calendarID,
+ }, nil
+}
+
+func (c *GoogleCalendarClient) GetUpcomingEvents(ctx context.Context, maxResults int) ([]models.CalendarEvent, error) {
+ t := time.Now().Format(time.RFC3339)
+ events, err := c.srv.Events.List(c.calendarID).ShowDeleted(false).
+ SingleEvents(true).TimeMin(t).MaxResults(int64(maxResults)).OrderBy("startTime").Do()
+ if err != nil {
+ return nil, fmt.Errorf("unable to retrieve events: %v", err)
+ }
+
+ var calendarEvents []models.CalendarEvent
+ for _, item := range events.Items {
+ var start, end time.Time
+ if item.Start.DateTime == "" {
+ // All-day event
+ start, _ = time.Parse("2006-01-02", item.Start.Date)
+ end, _ = time.Parse("2006-01-02", item.End.Date)
+ } else {
+ start, _ = time.Parse(time.RFC3339, item.Start.DateTime)
+ end, _ = time.Parse(time.RFC3339, item.End.DateTime)
+ }
+
+ calendarEvents = append(calendarEvents, models.CalendarEvent{
+ ID: item.Id,
+ Summary: item.Summary,
+ Description: item.Description,
+ Start: start,
+ End: end,
+ HTMLLink: item.HtmlLink,
+ })
+ }
+
+ return calendarEvents, nil
+}
diff --git a/internal/api/interfaces.go b/internal/api/interfaces.go
index 32d0120..e2521f4 100644
--- a/internal/api/interfaces.go
+++ b/internal/api/interfaces.go
@@ -34,9 +34,15 @@ type PlanToEatAPI interface {
AddMealToPlanner(ctx context.Context, recipeID string, date time.Time, mealType string) error
}
+// GoogleCalendarAPI defines the interface for Google Calendar operations
+type GoogleCalendarAPI interface {
+ GetUpcomingEvents(ctx context.Context, maxResults int) ([]models.CalendarEvent, error)
+}
+
// Ensure concrete types implement interfaces
var (
- _ TodoistAPI = (*TodoistClient)(nil)
- _ TrelloAPI = (*TrelloClient)(nil)
- _ PlanToEatAPI = (*PlanToEatClient)(nil)
+ _ TodoistAPI = (*TodoistClient)(nil)
+ _ TrelloAPI = (*TrelloClient)(nil)
+ _ PlanToEatAPI = (*PlanToEatClient)(nil)
+ _ GoogleCalendarAPI = (*GoogleCalendarClient)(nil)
)
diff --git a/internal/config/config.go b/internal/config/config.go
index 662159e..ba2719d 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -14,6 +14,10 @@ type Config struct {
TrelloAPIKey string
TrelloToken string
+ // Google Calendar
+ GoogleCredentialsFile string
+ GoogleCalendarID string
+
// Paths
DatabasePath string
TemplateDir string
@@ -34,6 +38,10 @@ func Load() (*Config, error) {
TrelloAPIKey: os.Getenv("TRELLO_API_KEY"),
TrelloToken: os.Getenv("TRELLO_TOKEN"),
+ // Google Calendar
+ GoogleCredentialsFile: os.Getenv("GOOGLE_CREDENTIALS_FILE"),
+ GoogleCalendarID: getEnvWithDefault("GOOGLE_CALENDAR_ID", "primary"),
+
// Paths
DatabasePath: getEnvWithDefault("DATABASE_PATH", "./dashboard.db"),
TemplateDir: getEnvWithDefault("TEMPLATE_DIR", "web/templates"),
@@ -81,6 +89,11 @@ func (c *Config) HasTrello() bool {
return c.TrelloAPIKey != "" && c.TrelloToken != ""
}
+// HasGoogleCalendar checks if Google Calendar is configured
+func (c *Config) HasGoogleCalendar() bool {
+ return c.GoogleCredentialsFile != ""
+}
+
// getEnvWithDefault returns environment variable value or default if not set
func getEnvWithDefault(key, defaultValue string) string {
if value := os.Getenv(key); value != "" {
diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go
index 19415c7..1cb978d 100644
--- a/internal/handlers/handlers.go
+++ b/internal/handlers/handlers.go
@@ -22,16 +22,17 @@ import (
// Handler holds dependencies for HTTP handlers
type Handler struct {
- store *store.Store
- todoistClient api.TodoistAPI
- trelloClient api.TrelloAPI
- planToEatClient api.PlanToEatAPI
- config *config.Config
- templates *template.Template
+ store *store.Store
+ todoistClient api.TodoistAPI
+ trelloClient api.TrelloAPI
+ planToEatClient api.PlanToEatAPI
+ googleCalendarClient api.GoogleCalendarAPI
+ config *config.Config
+ templates *template.Template
}
// New creates a new Handler instance
-func New(s *store.Store, todoist api.TodoistAPI, trello api.TrelloAPI, planToEat api.PlanToEatAPI, cfg *config.Config) *Handler {
+func New(s *store.Store, todoist api.TodoistAPI, trello api.TrelloAPI, planToEat api.PlanToEatAPI, googleCalendar api.GoogleCalendarAPI, cfg *config.Config) *Handler {
// Parse templates including partials
tmpl, err := template.ParseGlob(filepath.Join(cfg.TemplateDir, "*.html"))
if err != nil {
@@ -45,12 +46,13 @@ func New(s *store.Store, todoist api.TodoistAPI, trello api.TrelloAPI, planToEat
}
return &Handler{
- store: s,
- todoistClient: todoist,
- trelloClient: trello,
- planToEatClient: planToEat,
- config: cfg,
- templates: tmpl,
+ store: s,
+ todoistClient: todoist,
+ trelloClient: trello,
+ planToEatClient: planToEat,
+ googleCalendarClient: googleCalendar,
+ config: cfg,
+ templates: tmpl,
}
}
@@ -279,6 +281,22 @@ func (h *Handler) aggregateData(ctx context.Context, forceRefresh bool) (*models
}()
}
+ // Fetch Google Calendar events (if configured)
+ if h.googleCalendarClient != nil {
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ events, err := h.googleCalendarClient.GetUpcomingEvents(ctx, 10)
+ mu.Lock()
+ defer mu.Unlock()
+ if err != nil {
+ data.Errors = append(data.Errors, "Google Calendar: "+err.Error())
+ } else {
+ data.Events = events
+ }
+ }()
+ }
+
wg.Wait()
// Filter Trello cards into tasks based on heuristic
diff --git a/internal/handlers/heuristic_test.go b/internal/handlers/heuristic_test.go
index dc8620a..2b70218 100644
--- a/internal/handlers/heuristic_test.go
+++ b/internal/handlers/heuristic_test.go
@@ -63,7 +63,7 @@ func TestHandleTasks_Heuristic(t *testing.T) {
}
// Create Handler
- h := NewTabsHandler(db, "../../web/templates")
+ h := NewTabsHandler(db, nil, "../../web/templates")
// Skip if templates are not loaded
if h.templates == nil {
diff --git a/internal/handlers/tab_state_test.go b/internal/handlers/tab_state_test.go
index a4f6d23..d7bb8dd 100644
--- a/internal/handlers/tab_state_test.go
+++ b/internal/handlers/tab_state_test.go
@@ -30,7 +30,7 @@ func TestHandleDashboard_TabState(t *testing.T) {
}
// Create handler
- h := New(db, todoistClient, trelloClient, nil, cfg)
+ h := New(db, todoistClient, trelloClient, nil, nil, cfg)
// Skip if templates are not loaded (test environment issue)
if h.templates == nil {
diff --git a/internal/handlers/tabs.go b/internal/handlers/tabs.go
index 2f22c44..b651dac 100644
--- a/internal/handlers/tabs.go
+++ b/internal/handlers/tabs.go
@@ -9,6 +9,7 @@ import (
"strings"
"time"
+ "task-dashboard/internal/api"
"task-dashboard/internal/models"
"task-dashboard/internal/store"
)
@@ -46,12 +47,13 @@ func atomUrgencyTier(a models.Atom) int {
// TabsHandler handles tab-specific rendering with Atom model
type TabsHandler struct {
- store *store.Store
- templates *template.Template
+ store *store.Store
+ googleCalendarClient api.GoogleCalendarAPI
+ templates *template.Template
}
// NewTabsHandler creates a new TabsHandler instance
-func NewTabsHandler(store *store.Store, templateDir string) *TabsHandler {
+func NewTabsHandler(store *store.Store, googleCalendarClient api.GoogleCalendarAPI, templateDir string) *TabsHandler {
// Parse templates including partials
tmpl, err := template.ParseGlob(filepath.Join(templateDir, "*.html"))
if err != nil {
@@ -65,8 +67,9 @@ func NewTabsHandler(store *store.Store, templateDir string) *TabsHandler {
}
return &TabsHandler{
- store: store,
- templates: tmpl,
+ store: store,
+ googleCalendarClient: googleCalendarClient,
+ templates: tmpl,
}
}
@@ -178,12 +181,25 @@ func (h *TabsHandler) HandlePlanning(w http.ResponseWriter, r *http.Request) {
return
}
+ // Fetch Google Calendar events
+ var events []models.CalendarEvent
+ if h.googleCalendarClient != nil {
+ var err error
+ events, err = h.googleCalendarClient.GetUpcomingEvents(r.Context(), 10)
+ if err != nil {
+ log.Printf("Error fetching calendar events: %v", err)
+ // Don't fail the whole request, just show empty events
+ }
+ }
+
data := struct {
Boards []models.Board
Projects []models.Project
+ Events []models.CalendarEvent
}{
Boards: boards,
Projects: []models.Project{}, // Empty for now
+ Events: events,
}
if err := h.templates.ExecuteTemplate(w, "planning-tab", data); err != nil {
diff --git a/internal/models/types.go b/internal/models/types.go
index d9e955b..a604b28 100644
--- a/internal/models/types.go
+++ b/internal/models/types.go
@@ -57,6 +57,16 @@ type Project struct {
Name string `json:"name"`
}
+// CalendarEvent represents a Google Calendar event
+type CalendarEvent struct {
+ ID string `json:"id"`
+ Summary string `json:"summary"`
+ Description string `json:"description"`
+ Start time.Time `json:"start"`
+ End time.Time `json:"end"`
+ HTMLLink string `json:"html_link"`
+}
+
// CacheMetadata tracks when data was last fetched
type CacheMetadata struct {
Key string `json:"key"`
@@ -72,11 +82,12 @@ func (cm *CacheMetadata) IsCacheValid() bool {
// DashboardData aggregates all data for the main view
type DashboardData struct {
- Tasks []Task `json:"tasks"`
- Meals []Meal `json:"meals"`
- Boards []Board `json:"boards,omitempty"`
- TrelloTasks []Card `json:"trello_tasks,omitempty"`
- Projects []Project `json:"projects,omitempty"`
- LastUpdated time.Time `json:"last_updated"`
- Errors []string `json:"errors,omitempty"`
+ Tasks []Task `json:"tasks"`
+ Meals []Meal `json:"meals"`
+ Boards []Board `json:"boards,omitempty"`
+ TrelloTasks []Card `json:"trello_tasks,omitempty"`
+ Projects []Project `json:"projects,omitempty"`
+ Events []CalendarEvent `json:"events,omitempty"`
+ LastUpdated time.Time `json:"last_updated"`
+ Errors []string `json:"errors,omitempty"`
}