| Age | Commit message (Collapse) | Author |
|
With the cancelled-context bug fixed, the refresh actually reaches Google and
the real state of the Google Tasks grant becomes visible: it answers
invalid_grant. The stored refresh token has been dead since roughly 2026-08-16.
The earlier bug had been masking this — the refresh never got far enough to be
rejected.
Settings treated the presence of a token row as "Connected", so it reported
Google Tasks as connected for a month while every fetch failed. A refresh
failure is now persisted (migration 035 adds last_error/last_error_at to
oauth_tokens) and cleared on the next success, and settings reports "connected"
only when there is no recorded failure.
invalid_grant is classified specially, because it is the one failure retrying
cannot fix: the message tells the user to reconnect, and notes that a weekly
recurrence means the OAuth client is still in Testing publishing status, where
Google expires refresh tokens after seven days. Other errors — a network blip,
a 5xx — are left as transient rather than pushing the user through consent
needlessly.
Reconnecting at /settings is still a manual step; nothing here can perform
consent on the user's behalf.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018AwnALZL6bD4VBtATDVv3B
|
|
main.go builds the Tasks client with a short-lived init context and calls
cancel() the moment construction returns. dbTokenSource captured that context
and reused it for every subsequent token refresh, so the first refresh after
the startup access token aged out — roughly an hour in — failed with
"context canceled", and so did every one after it.
That is the whole reason Google Tasks has not been working: all four task
lists, the same error on every fetch, recovering only for an hour after each
restart. The message pointed at a transport problem, which is why it read as
flaky networking rather than a lifetime bug.
A token refresh is not logically part of whichever request happened to trigger
it, so it now gets its own background context with its own timeout. oauth2's
client is likewise built on context.Background(), since NewClient binds the
context it is given to the returned transport for its lifetime; per-call
contexts still come from the tasks service methods.
Regression test drives a refresh through a stub token endpoint with an
already-cancelled construction context, which fails on the old code with
exactly the production error. Also covers Google omitting refresh_token on a
refresh response, which would otherwise blank the stored one.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018AwnALZL6bD4VBtATDVv3B
|
|
Service-account auth structurally cannot see a regular user's personal
task lists (no equivalent of Calendar's per-item sharing model) --
confirmed via GetTaskLists returning exactly the service account's own
empty "My Tasks" list, never the real user's three lists. Zero rows
were ever cached in production as a result.
Adds a standard 3-legged OAuth flow: /settings/google-tasks/connect
redirects to Google's consent screen (AccessTypeOffline+ApprovalForce
so a refresh_token is always issued), /callback exchanges the code and
persists the token (new oauth_tokens table), /disconnect clears it.
GoogleTasksClient now takes an option.ClientOption instead of a
credentials file path; NewGoogleTasksOAuthClient wraps it with a
dbTokenSource that reloads/refreshes from the DB on each access-token
expiry and re-persists -- carefully preserving the original
refresh_token when Google's refresh response omits one (it usually
does), which would otherwise silently and permanently break future
refreshes.
Settings page shows connection status and a Connect/Disconnect
button. Calendar keeps using service-account auth (that one actually
works). Requires a one-time manual step: create an OAuth 2.0 Client ID
in Google Cloud Console and set GOOGLE_OAUTH_CLIENT_ID/SECRET in .env
-- documented in .env.example.
|