From e97a1bc259d3aa91956ec73a522421cdb621ae57 Mon Sep 17 00:00:00 2001 From: Peter Stone Date: Thu, 22 Jan 2026 15:28:06 -1000 Subject: 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 --- app | Bin 18004488 -> 0 bytes cmd/dashboard/main.go | 15 +++++-- go.mod | 27 ++++++++++++ go.sum | 73 ++++++++++++++++++++++++++++++++ internal/api/google_calendar.go | 62 +++++++++++++++++++++++++++ internal/api/interfaces.go | 12 ++++-- internal/config/config.go | 13 ++++++ internal/handlers/handlers.go | 44 +++++++++++++------ internal/handlers/heuristic_test.go | 2 +- internal/handlers/tab_state_test.go | 2 +- internal/handlers/tabs.go | 26 +++++++++--- internal/models/types.go | 25 ++++++++--- migrations/006_remove_resolved_bugs.sql | 2 +- test/acceptance_test.go | 2 +- 14 files changed, 270 insertions(+), 35 deletions(-) delete mode 100755 app create mode 100644 internal/api/google_calendar.go diff --git a/app b/app deleted file mode 100755 index 1881b0f..0000000 Binary files a/app and /dev/null differ diff --git a/cmd/dashboard/main.go b/cmd/dashboard/main.go index fd2c024..3d8b330 100644 --- a/cmd/dashboard/main.go +++ b/cmd/dashboard/main.go @@ -83,9 +83,18 @@ func main() { planToEatClient = api.NewPlanToEatClient(cfg.PlanToEatAPIKey) } + var googleCalendarClient api.GoogleCalendarAPI + if cfg.HasGoogleCalendar() { + var err error + googleCalendarClient, err = api.NewGoogleCalendarClient(context.Background(), cfg.GoogleCredentialsFile, cfg.GoogleCalendarID) + if err != nil { + log.Printf("Warning: failed to initialize Google Calendar client: %v", err) + } + } + // Initialize handlers - h := handlers.New(db, todoistClient, trelloClient, planToEatClient, cfg) - tabsHandler := handlers.NewTabsHandler(db, cfg.TemplateDir) + h := handlers.New(db, todoistClient, trelloClient, planToEatClient, googleCalendarClient, cfg) + tabsHandler := handlers.NewTabsHandler(db, googleCalendarClient, cfg.TemplateDir) // Set up router r := chi.NewRouter() @@ -94,7 +103,7 @@ func main() { r.Use(middleware.Logger) r.Use(middleware.Recoverer) r.Use(middleware.Timeout(60 * time.Second)) - r.Use(sessionManager.LoadAndSave) // Session middleware must be applied globally + r.Use(sessionManager.LoadAndSave) // Session middleware must be applied globally r.Use(authHandlers.Middleware().CSRFProtect) // CSRF protection // Public routes (no auth required) diff --git a/go.mod b/go.mod index 330550e..9aa4fb9 100644 --- a/go.mod +++ b/go.mod @@ -13,4 +13,31 @@ require ( github.com/alexedwards/scs/v2 v2.9.0 github.com/joho/godotenv v1.5.1 golang.org/x/crypto v0.47.0 + google.golang.org/api v0.262.0 +) + +require ( + cloud.google.com/go/auth v0.18.1 // indirect + cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect + cloud.google.com/go/compute/metadata v0.9.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/google/s2a-go v0.1.9 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.11 // indirect + github.com/googleapis/gax-go/v2 v2.16.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect + go.opentelemetry.io/otel v1.39.0 // indirect + go.opentelemetry.io/otel/metric v1.39.0 // indirect + go.opentelemetry.io/otel/trace v1.39.0 // indirect + golang.org/x/net v0.49.0 // indirect + golang.org/x/oauth2 v0.34.0 // indirect + golang.org/x/sys v0.40.0 // indirect + golang.org/x/text v0.33.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260120174246-409b4a993575 // indirect + google.golang.org/grpc v1.78.0 // indirect + google.golang.org/protobuf v1.36.11 // indirect ) diff --git a/go.sum b/go.sum index f153255..9820bc5 100644 --- a/go.sum +++ b/go.sum @@ -1,16 +1,89 @@ +cloud.google.com/go/auth v0.18.1 h1:IwTEx92GFUo2pJ6Qea0EU3zYvKnTAeRCODxfA/G5UWs= +cloud.google.com/go/auth v0.18.1/go.mod h1:GfTYoS9G3CWpRA3Va9doKN9mjPGRS+v41jmZAhBzbrA= +cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= +cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= +cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= +cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU= github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= github.com/alexedwards/scs/sqlite3store v0.0.0-20251002162104-209de6e426de h1:c72K9HLu6K442et0j3BUL/9HEYaUJouLkkVANdmqTOo= github.com/alexedwards/scs/sqlite3store v0.0.0-20251002162104-209de6e426de/go.mod h1:Iyk7S76cxGaiEX/mSYmTZzYehp4KfyylcLaV3OnToss= github.com/alexedwards/scs/v2 v2.9.0 h1:xa05mVpwTBm1iLeTMNFfAWpKUm4fXAW7CeAViqBVS90= github.com/alexedwards/scs/v2 v2.9.0/go.mod h1:ToaROZxyKukJKT/xLcVQAChi5k6+Pn1Gvmdl7h3RRj8= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE= github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= +github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/enterprise-certificate-proxy v0.3.11 h1:vAe81Msw+8tKUxi2Dqh/NZMz7475yUvmRIkXr4oN2ao= +github.com/googleapis/enterprise-certificate-proxy v0.3.11/go.mod h1:RFV7MUdlb7AgEq2v7FmMCfeSMCllAzWxFgRdusoGks8= +github.com/googleapis/gax-go/v2 v2.16.0 h1:iHbQmKLLZrexmb0OSsNGTeSTS0HO4YvFOG8g5E4Zd0Y= +github.com/googleapis/gax-go/v2 v2.16.0/go.mod h1:o1vfQjjNZn4+dPnRdl/4ZD7S9414Y4xA+a/6Icj6l14= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE= github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/mattn/go-sqlite3 v1.14.33 h1:A5blZ5ulQo2AtayQ9/limgHEkFreKj1Dv226a1K73s0= github.com/mattn/go-sqlite3 v1.14.33/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q= +go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= +go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= +go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= +go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= +go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= +go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= +go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= +go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= +go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= +go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= +golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= +golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= +golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= +golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= +golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= +golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +google.golang.org/api v0.262.0 h1:4B+3u8He2GwyN8St3Jhnd3XRHlIvc//sBmgHSp78oNY= +google.golang.org/api v0.262.0/go.mod h1:jNwmH8BgUBJ/VrUG6/lIl9YiildyLd09r9ZLHiQ6cGI= +google.golang.org/genproto v0.0.0-20251202230838-ff82c1b0f217 h1:GvESR9BIyHUahIb0NcTum6itIWtdoglGX+rnGxm2934= +google.golang.org/genproto v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:yJ2HH4EHEDTd3JiLmhds6NkJ17ITVYOdV3m3VKOnws0= +google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 h1:fCvbg86sFXwdrl5LgVcTEvNC+2txB5mgROGmRL5mrls= +google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260120174246-409b4a993575 h1:vzOYHDZEHIsPYYnaSYo60AqHkJronSu0rzTz/s4quL0= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260120174246-409b4a993575/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= +google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc= +google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 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"` } diff --git a/migrations/006_remove_resolved_bugs.sql b/migrations/006_remove_resolved_bugs.sql index 666886b..344f4c3 100644 --- a/migrations/006_remove_resolved_bugs.sql +++ b/migrations/006_remove_resolved_bugs.sql @@ -1,2 +1,2 @@ -- Remove bugs that have been resolved -DELETE FROM bugs WHERE id IN (1, 2, 3, 4, 5, 6, 7, 9, 10, 14, 15, 16); +DELETE FROM bugs WHERE id IN (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 14, 15, 16); diff --git a/test/acceptance_test.go b/test/acceptance_test.go index 8d73d14..ef8afae 100644 --- a/test/acceptance_test.go +++ b/test/acceptance_test.go @@ -82,7 +82,7 @@ func setupTestServer(t *testing.T) (*httptest.Server, *store.Store, *http.Client } // Initialize handlers - h := handlers.New(db, todoistClient, trelloClient, nil, cfg) + h := handlers.New(db, todoistClient, trelloClient, nil, nil, cfg) // Set up router (same as main.go) r := chi.NewRouter() -- cgit v1.2.3