Skip to content

google sync: expired connection shows misleading revoked and repair states #1822

@tyler-dane

Description

@tyler-dane

Bug report

When a Google-connected user's session/Google sync has expired or stopped, the first visit to Compass can show a destructive-sounding toast: "Google access revoked. Your Google data has been removed." In the reported case the user did not manually revoke access in Google, so the message makes it sound like they did something wrong. The UI also does not immediately settle into the accurate reconnect state until the user manually refreshes, and after reconnect the sidebar/header can briefly show "Repair needed" even though Compass is already repairing in the background.

This affects returning Google-connected users whose stored Google connection is no longer healthy, especially after an expired session or stopped sync.

Where it happened

  • Hosted/staging Compass day view: /day/2026-05-29
  • Google Calendar integration status UI: toast, left sidebar account summary, header info icon, and command palette reconnect flow
  • Account state: previously Google-connected; session/sync had expired or stopped; reporter says the user did not manually revoke Google access

Workflow

  1. Start from a Google-connected account whose session has expired and whose Google sync has stopped.
  2. Open Compass for the first time after that state.
  3. Observe the toast and Google status UI.
  4. Manually refresh the page.
  5. Open the command palette, choose the Reconnect Google option, and complete the OAuth flow successfully.
  6. Observe the left sidebar and top-right info icon immediately after returning from OAuth.
  7. Wait for the background work to finish.

Expected outcome

  • The toast should explain the real user-facing problem without asserting that the user revoked access. Suggested direction: say the Google Calendar connection is no longer active/healthy, Compass could not keep calendar data current, and the user needs to reconnect.
  • The app should update to the reconnect-needed state without requiring a manual reload.
  • After a successful reconnect, while Compass is already importing/repairing in the background, the sidebar/header should show a syncing/repairing-in-progress state rather than "Repair needed" or a tooltip that implies the user must take another action.

Screenshots

Image
  • Initial state: day view with a bottom-left toast reading "Google access revoked. Your Google data has been removed." Calendar appears empty/weird and top-right info icon is red.
Image - After manual refresh: same day view shows Google events again, sidebar account summary says "Reconnect needed," and top-right info icon tooltip/status indicates Google Calendar needs reconnecting.

Diagnosis

Not fully reproduced end-to-end locally because this requires a hosted account with expired/stopped Google sync and OAuth credentials. Targeted code inspection matches the reported behavior with medium-high confidence.

Evidence:

  • packages/web/src/auth/google/util/google.auth.util.ts hardcodes the misleading toast in handleGoogleRevoked: "Google access revoked. Your Google data has been removed." That handler is used both by REST error handling and the GOOGLE_REVOKED SSE path.
  • The same handler clears auth/user metadata (authSlice.actions.resetAuth() and userMetadataSlice.actions.clear(undefined)), removes Google-origin events, triggers a fetch, and reconnects SSE. It does not locally set metadata to google.connectionState = "RECONNECT_REQUIRED", so status can remain missing/stale until metadata is fetched or replayed.
  • packages/web/src/sse/hooks/useGcalSSE.ts stores USER_METADATA, clears the sync override on GOOGLE_REVOKED, and clears the override on IMPORT_GCAL_END. It only shows explicit in-progress UI when IMPORT_GCAL_START sets the sync override or metadata says IMPORTING.
  • packages/web/src/auth/google/hooks/useConnectGoogle/useConnectGoogle.ts maps transient sync overrides to repairing/IMPORTING; otherwise it uses server-derived connectionState. If reconnect returns with server metadata temporarily assessed as ATTENTION, useConnectGoogle.util.ts renders "Repair Google Calendar" / "Repair needed" even if backend repair/import is already running but the client has not yet received IMPORT_GCAL_START or IMPORTING metadata.
  • packages/backend/src/user/services/user-metadata.service.ts returns RECONNECT_REQUIRED when a Google user has no refresh token, IMPORTING when sync.importGCal === "IMPORTING", ATTENTION for RESTART or unhealthy sync, and HEALTHY once sync health passes. This explains the post-refresh reconnect state and the temporary repair state after OAuth.
  • Existing docs currently describe the manual revocation path only: docs/Acceptance/google-sync.md Scenario 7 expects the exact revoked toast and says revocation only happens when the user removes access in Google settings. The reported expired/stopped-session path is a distinct UX case that should not reuse that copy.

Related but not duplicate: #1722 (self-healing Google Watch repair) and #1719 (Google sync architecture PRD) are broader sync architecture work, not this concrete user-facing state/copy bug.

Baseline command run: bun test:web --filter useGcalSSE completed successfully. The runner executed the web package tests, including the current useGcalSSE, google.auth.util, useConnectGoogle.util, PlannerAccountSummary, and HeaderInfoIcon tests.

Likely fix

Start in these files:

  • packages/web/src/auth/google/util/google.auth.util.ts
  • packages/web/src/sse/hooks/useGcalSSE.ts
  • packages/web/src/auth/google/hooks/useConnectGoogle/useConnectGoogle.ts
  • packages/web/src/auth/google/hooks/useConnectGoogle/useConnectGoogle.util.ts
  • packages/backend/src/user/services/user-metadata.service.ts if the server needs to distinguish reconnect-required vs manual revocation vs background repair/import more explicitly

Concrete handoff:

  • Replace the revoked toast copy with neutral reconnect-required language for the generic invalid/unusable Google credential path. Avoid saying the user revoked access unless Compass has a reliable signal that the user actually removed access in Google.
  • On GOOGLE_REVOKED/invalid Google credential handling, make the client immediately render the reconnect-needed state without waiting for a manual refresh. Options include refreshing user metadata immediately and/or applying a local metadata state of { google: { connectionState: "RECONNECT_REQUIRED" } } after pruning, while preserving the Google-origin event removal/refetch behavior.
  • During reconnect OAuth, ensure the UI enters a syncing/repairing-in-progress state as soon as the reconnect flow starts or returns successfully, and keep that state until IMPORT_GCAL_END or fresh metadata reports HEALTHY. Do not show actionable "Repair needed" while the backend has already started repair/import work.
  • Add regression tests covering: misleading toast copy, immediate reconnect-needed UI after GOOGLE_REVOKED, and post-reconnect background repair/import showing syncing/repairing instead of actionable repair.
  • Keep the true manual Google revocation behavior safe: Google-origin events may still be pruned when credentials are unusable, and users still need a clear reconnect path.

Verification

Shortest practical verification:

  1. Unit/interaction tests:
    • bun test:web or focused tests for google.auth.util, useGcalSSE, useConnectGoogle, PlannerAccountSummary, and HeaderInfoIcon.
  2. Manual hosted/staging flow with a Google-connected test account:
    • Force stored Google credentials/sync into the expired or unusable state without manually revoking from Google, then open /day/2026-05-29.
    • Confirm the toast uses neutral reconnect copy and the sidebar/header show reconnect-needed without a manual reload.
    • Reconnect through Cmd+K OAuth.
    • Confirm sidebar/header show syncing/repairing in progress while background work runs, then "Synced with Google" after completion, with no temporary actionable "Repair needed" state.
  3. Manual true-revocation flow:
    • Remove access at myaccount.google.com/permissions and confirm Compass still prunes Google-origin events and guides the user to reconnect with accurate copy.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No fields configured for Bug.

    Projects

    Status
    Backlog

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions