summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPeter Stone <thepeterstone@gmail.com>2026-01-25 17:15:23 -1000
committerPeter Stone <thepeterstone@gmail.com>2026-01-25 17:15:23 -1000
commit4420aace9c0ee1bb3255190234f4a2035619b473 (patch)
treee5dc0fe1878984fbefeb012e235df896b63de4f0
parent6518a7ed5ee8800741351166a724da53cb2f271e (diff)
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 <noreply@anthropic.com>
-rw-r--r--internal/api/google_calendar.go65
-rw-r--r--internal/models/timeline.go8
-rw-r--r--web/templates/conditions.html10
3 files changed, 73 insertions, 10 deletions
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
diff --git a/web/templates/conditions.html b/web/templates/conditions.html
index 14b10b0..66f87ce 100644
--- a/web/templates/conditions.html
+++ b/web/templates/conditions.html
@@ -69,8 +69,8 @@
}
@media (max-width: 600px) {
.video-grid {
- grid-template-columns: 1fr;
- grid-template-rows: repeat(6, 1fr);
+ grid-template-columns: repeat(2, 1fr);
+ grid-template-rows: repeat(3, 1fr);
}
}
</style>
@@ -82,7 +82,7 @@
<!-- V1cam - Kilauea West Rim -->
<div class="video-cell">
<iframe
- src="https://www.youtube.com/embed/WTy3dGhGBOY?autoplay=1&mute=1&controls=1&modestbranding=1&rel=0"
+ src="https://www.youtube.com/embed/tk0tfYDxrUA?autoplay=1&mute=1&controls=1&modestbranding=1&rel=0"
allow="autoplay; encrypted-media; picture-in-picture"
allowfullscreen>
</iframe>
@@ -92,7 +92,7 @@
<!-- V2cam - Kilauea East Rim -->
<div class="video-cell">
<iframe
- src="https://www.youtube.com/embed/Gd2Tm5jblbE?autoplay=1&mute=1&controls=1&modestbranding=1&rel=0"
+ src="https://www.youtube.com/embed/fiyttmA7YkA?autoplay=1&mute=1&controls=1&modestbranding=1&rel=0"
allow="autoplay; encrypted-media; picture-in-picture"
allowfullscreen>
</iframe>
@@ -102,7 +102,7 @@
<!-- V3cam - Kilauea South Rim -->
<div class="video-cell">
<iframe
- src="https://www.youtube.com/embed/BqmpkUdMtyA?autoplay=1&mute=1&controls=1&modestbranding=1&rel=0"
+ src="https://www.youtube.com/embed/gXKuUyKt8mc?autoplay=1&mute=1&controls=1&modestbranding=1&rel=0"
allow="autoplay; encrypted-media; picture-in-picture"
allowfullscreen>
</iframe>