From 4420aace9c0ee1bb3255190234f4a2035619b473 Mon Sep 17 00:00:00 2001 From: Peter Stone Date: Sun, 25 Jan 2026 17:15:23 -1000 Subject: Fix timezone and date handling bugs #40, #41, #42 #40, #41: Fix calendar event timezone handling - Parse all-day events in local timezone using ParseInLocation - Convert timed events to local time after parsing RFC3339 - Update ComputeDaySection to normalize both now and item time to local before comparison, ensuring consistent today/tomorrow classification #42: Mobile conditions page now uses 2 columns - Changed 600px breakpoint from 1 column to 2 columns Co-Authored-By: Claude Opus 4.5 --- internal/api/google_calendar.go | 65 +++++++++++++++++++++++++++++++++++++++-- internal/models/timeline.go | 8 +++-- 2 files changed, 68 insertions(+), 5 deletions(-) (limited to 'internal') diff --git a/internal/api/google_calendar.go b/internal/api/google_calendar.go index 919976b..8217b49 100644 --- a/internal/api/google_calendar.go +++ b/internal/api/google_calendar.go @@ -57,12 +57,15 @@ func (c *GoogleCalendarClient) GetUpcomingEvents(ctx context.Context, maxResults 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) + // All-day event - parse in local timezone + start, _ = time.ParseInLocation("2006-01-02", item.Start.Date, time.Local) + end, _ = time.ParseInLocation("2006-01-02", item.End.Date, time.Local) } else { + // Timed event - parse RFC3339 then convert to local start, _ = time.Parse(time.RFC3339, item.Start.DateTime) end, _ = time.Parse(time.RFC3339, item.End.DateTime) + start = start.Local() + end = end.Local() } allEvents = append(allEvents, models.CalendarEvent{ @@ -101,3 +104,59 @@ func (c *GoogleCalendarClient) GetUpcomingEvents(ctx context.Context, maxResults return uniqueEvents, nil } + +func (c *GoogleCalendarClient) GetEventsByDateRange(ctx context.Context, start, end time.Time) ([]models.CalendarEvent, error) { + timeMin := start.Format(time.RFC3339) + timeMax := end.Format(time.RFC3339) + var allEvents []models.CalendarEvent + + for _, calendarID := range c.calendarIDs { + events, err := c.srv.Events.List(calendarID).ShowDeleted(false). + SingleEvents(true).TimeMin(timeMin).TimeMax(timeMax).OrderBy("startTime").Do() + if err != nil { + log.Printf("Warning: failed to fetch events from calendar %s: %v", calendarID, err) + continue + } + + for _, item := range events.Items { + var evtStart, evtEnd time.Time + if item.Start.DateTime == "" { + // All-day event - parse in local timezone + evtStart, _ = time.ParseInLocation("2006-01-02", item.Start.Date, time.Local) + evtEnd, _ = time.ParseInLocation("2006-01-02", item.End.Date, time.Local) + } else { + // Timed event - parse RFC3339 then convert to local + evtStart, _ = time.Parse(time.RFC3339, item.Start.DateTime) + evtEnd, _ = time.Parse(time.RFC3339, item.End.DateTime) + evtStart = evtStart.Local() + evtEnd = evtEnd.Local() + } + + allEvents = append(allEvents, models.CalendarEvent{ + ID: item.Id, + Summary: item.Summary, + Description: item.Description, + Start: evtStart, + End: evtEnd, + HTMLLink: item.HtmlLink, + }) + } + } + + // Deduplicate + seen := make(map[string]bool) + var uniqueEvents []models.CalendarEvent + for _, event := range allEvents { + key := fmt.Sprintf("%s|%d", event.Summary, event.Start.Unix()) + if !seen[key] { + seen[key] = true + uniqueEvents = append(uniqueEvents, event) + } + } + + sort.Slice(uniqueEvents, func(i, j int) bool { + return uniqueEvents[i].Start.Before(uniqueEvents[j].Start) + }) + + return uniqueEvents, nil +} diff --git a/internal/models/timeline.go b/internal/models/timeline.go index 469cce4..54f7f45 100644 --- a/internal/models/timeline.go +++ b/internal/models/timeline.go @@ -37,11 +37,15 @@ type TimelineItem struct { // ComputeDaySection sets the DaySection based on the item's time func (item *TimelineItem) ComputeDaySection(now time.Time) { - today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()) + // Ensure we're working in local timezone for consistent comparisons + localNow := now.Local() + localItemTime := item.Time.Local() + + today := time.Date(localNow.Year(), localNow.Month(), localNow.Day(), 0, 0, 0, 0, time.Local) tomorrow := today.AddDate(0, 0, 1) dayAfterTomorrow := today.AddDate(0, 0, 2) - itemDay := time.Date(item.Time.Year(), item.Time.Month(), item.Time.Day(), 0, 0, 0, 0, item.Time.Location()) + itemDay := time.Date(localItemTime.Year(), localItemTime.Month(), localItemTime.Day(), 0, 0, 0, 0, time.Local) if itemDay.Before(tomorrow) { item.DaySection = DaySectionToday -- cgit v1.2.3