diff options
| author | Peter Stone <thepeterstone@gmail.com> | 2026-07-28 02:37:23 +0000 |
|---|---|---|
| committer | Peter Stone <thepeterstone@gmail.com> | 2026-07-28 02:37:23 +0000 |
| commit | 2da86b009c76bb7688103da49a9125d35d3a1ed8 (patch) | |
| tree | bb9e5107d05826563e85f23b1a3d1488e378530d | |
| parent | 7f8cd6bfb894385b9ae310fd000cb921fef71cea (diff) | |
fix: Google Tasks due dates shifted a day earlier in negative-offset timezones
Google Tasks' due field is date-only but always serialized as an
RFC3339 timestamp fixed at midnight UTC regardless of the user's
timezone. Converting that instant into displayTZ (dueDate.In(tz))
reinterpreted it as a real moment in time instead of re-anchoring the
same calendar date to local midnight -- midnight UTC becomes 2pm the
previous day in UTC-10 (Pacific/Honolulu), so every dated Google Task
silently landed one day earlier than its actual due date. A task due
tomorrow showed up in today's section.
Fix: extract the Y/M/D from the UTC-anchored timestamp (which IS the
intended calendar date) and rebuild midnight in displayTZ from those
components, instead of converting the instant.
| -rw-r--r-- | internal/api/google_tasks.go | 15 | ||||
| -rw-r--r-- | internal/api/google_tasks_test.go | 40 |
2 files changed, 53 insertions, 2 deletions
diff --git a/internal/api/google_tasks.go b/internal/api/google_tasks.go index b580be4..1f9aefb 100644 --- a/internal/api/google_tasks.go +++ b/internal/api/google_tasks.go @@ -95,14 +95,25 @@ func (c *GoogleTasksClient) getTasksFromList(ctx context.Context, listID string) // Parse due date if present (RFC3339 format, but date-only) if item.Due != "" { - // Google Tasks due dates are in RFC3339 format but typically just the date part + // Google Tasks due dates are date-only, but the API always + // serializes them as an RFC3339 timestamp fixed at midnight UTC + // regardless of the user's timezone. Converting that instant + // into displayTZ (dueDate.In(c.displayTZ)) reinterprets it as a + // real moment in time and shifts the calendar date backward for + // any timezone behind UTC -- midnight UTC becomes 2pm the + // PREVIOUS day in UTC-10, silently moving every dated Google + // Task one day earlier than its actual due date. The Y/M/D of + // the UTC-anchored timestamp IS the intended calendar date, so + // re-anchor those same components to displayTZ midnight instead + // of converting the instant. dueDate, err := time.Parse(time.RFC3339, item.Due) if err != nil { // Try date-only format dueDate, err = time.ParseInLocation("2006-01-02", item.Due[:10], c.displayTZ) } if err == nil { - dueInTZ := dueDate.In(c.displayTZ) + y, m, d := dueDate.Date() + dueInTZ := time.Date(y, m, d, 0, 0, 0, 0, c.displayTZ) task.DueDate = &dueInTZ } } diff --git a/internal/api/google_tasks_test.go b/internal/api/google_tasks_test.go index fbfdd63..bdb0187 100644 --- a/internal/api/google_tasks_test.go +++ b/internal/api/google_tasks_test.go @@ -185,3 +185,43 @@ func TestGetTasksByDateRange_OutOfRangeExcluded(t *testing.T) { t.Errorf("expected task-in-range, got %s", tasks[0].ID) } } + +// TestGetTasksFromList_DueDateNotShiftedByNegativeOffsetTZ guards against a +// regression where a task due "tomorrow" showed up "today" on the widget. +// Google Tasks' due field is date-only but always serialized as midnight +// UTC. Converting that instant into a negative-offset display timezone +// (e.g. Pacific/Honolulu, UTC-10) instead of re-anchoring the same calendar +// date to local midnight shifted every dated task back by one day. +func TestGetTasksFromList_DueDateNotShiftedByNegativeOffsetTZ(t *testing.T) { + honolulu, err := time.LoadLocation("Pacific/Honolulu") + if err != nil { + t.Skipf("Pacific/Honolulu tzdata not available: %v", err) + } + + body := tasksAPIResponse([]map[string]interface{}{ + { + "id": "task-tomorrow", + "title": "Due tomorrow", + "status": "needsAction", + "due": "2026-07-29T00:00:00.000Z", + "updated": "2026-07-28T00:00:00Z", + }, + }) + server := newTasksServer(body) + defer server.Close() + + client := newTestGoogleTasksClient(t, server, "@default", honolulu) + tasks, err := client.GetTasks(context.Background()) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(tasks) != 1 { + t.Fatalf("expected 1 task, got %d", len(tasks)) + } + if tasks[0].DueDate == nil { + t.Fatal("expected DueDate to be set") + } + if y, m, d := tasks[0].DueDate.Date(); y != 2026 || m != time.July || d != 29 { + t.Errorf("expected due date 2026-07-29, got %04d-%02d-%02d (task shifted a day earlier than Google reported)", y, m, d) + } +} |
