One-way push of the weekly dinner schedule into a shared Google Calendar, with the app as source of truth (requirements §8 [Decided]; owner chose Google OAuth in Settings — D-011; CTXD RUNBOOKS "Google Cloud setup").
Tasks (all five merged, each independently reviewed at its exact head)
Google OAuth from Settings: GET /api/v1/integrations/google/connect (session-bound state, PKCE, access_type=offline, prompt=consent), the callback exchanging the code with the file-sourced secret under MEALQUEST_PUBLIC_URL, the refresh token encrypted with secretbox, and DELETE revoking at Google and disconnecting. One grantedConnection decider reports not-connected / refused-grant / missing-calendar in a fixed order.
Calendar list and selection (GET .../calendars, PUT .../calendar) filtered to writer/owner, with one forgetEvents invalidation helper shared by a selection change and a disconnect, so no stored plan-day event id and no synced plan outlives the calendar it belongs to.
POST /api/v1/plans/{weekStart}/push-calendar writing one deterministic-id event per day (summary "Dinner: {name}", household dinner_time for 60 minutes, description with the crock-pot "Start by" and a dinner-page link), insert-then-update on 409, recording calendar_synced_at and each day's calendar_event_id under the account lock only after every write succeeds.
Token-failure handling: invalid_grant or a 401 after refresh on any Calendar call clears the token, sets last_sync_error, and yields 409 google_reconnect_required on push and reconnect_required from the new GET /api/v1/integrations/google; transient 429/5xx retry twice then surface a retryable 503 without disconnecting.
The Settings → Google Calendar screen (connect link, calendar picker, Connected · {calendar}, Disconnect with confirmation, grape reconnect banner) and the plan screens' real Push to Calendar buttons with progress and error states.
Integration checks at the feature head 081ad75fe256c99ae51082844242f3cd7e83be34 (5 commits ahead of dev, 0 behind)
gofmt -l . — empty; go vet ./... — clean
go test -race -count=1 ./... — 26 packages ok, 0 failures
CGO_ENABLED=0 go build ./... — ok
make sqlc-check SQLC=$(go env GOPATH)/bin/sqlc — no drift
cd web && npm run typecheck — PASS; npm test — PASS 46 files / 804 tests; npm run build — PASS
make test, make build, docker build . — PASS
The branch already contains all of dev, so no dev merge was needed.
Deployment prerequisite — please read
MEALQUEST_GOOGLE_CLIENT_ID and MEALQUEST_GOOGLE_CLIENT_SECRET_FILE are required at boot (fail-fast, matching the OIDC variables). A release containing this feature will not start until the owner creates the Google OAuth web client and sets the secret on the host. The steps are in the runbook section "Google Calendar OAuth client", and deploy/compose.yaml / deploy/.env.example already carry the mount and variables.
Notes
Migration 0015_google_reconnect_required.sql makes integrations_google.refresh_token_ct nullable with a CHECK that a row without a token carries a last_sync_error. The independent release-style review examined it and confirmed it is safe to apply to the existing household database at boot.
Non-blocking findings were dispositioned and are tracked as Maestro task d21e4a83 in Feature 22 (the same plan-screen hardening task, which the push mutation's retention fix has now partly closed).
The real production push to the household "Dinners" calendar with a screenshot was split out into Maestro task 9a4d2601 in Feature 22 (owner-dependent: it needs the real OAuth client and the deployed service).
## Feature 16 — Google Calendar Sync
Integration pull request: `feature/c53328cb-577c-4aea-b4b2-817acfc3dfeb-google-calendar-sync` -> `dev`.
One-way push of the weekly dinner schedule into a shared Google Calendar, with the app as source of truth (requirements §8 [Decided]; owner chose Google OAuth in Settings — D-011; CTXD RUNBOOKS "Google Cloud setup").
## Tasks (all five merged, each independently reviewed at its exact head)
| Task | Maestro | PR | Reviewed head |
| --- | --- | --- | --- |
| Google OAuth connect and disconnect | 1e9d7850 | #125 | 2cd2e5a |
| Calendar list and selection | 0fe3f1f2 | #126 | d23d30e |
| Push plan to Google Calendar | 5885d2b1 | #127 | fb7d8dd |
| Token failure handling | b8d8da5a | #128 | ee67d52 |
| Calendar settings screen and plan sync status | 677261bc | #129 | 3a1884d |
## What the feature delivers
- Google OAuth from Settings: `GET /api/v1/integrations/google/connect` (session-bound state, PKCE, `access_type=offline`, `prompt=consent`), the callback exchanging the code with the file-sourced secret under `MEALQUEST_PUBLIC_URL`, the refresh token encrypted with secretbox, and `DELETE` revoking at Google and disconnecting. One `grantedConnection` decider reports not-connected / refused-grant / missing-calendar in a fixed order.
- Calendar list and selection (`GET .../calendars`, `PUT .../calendar`) filtered to writer/owner, with one `forgetEvents` invalidation helper shared by a selection change and a disconnect, so no stored plan-day event id and no `synced` plan outlives the calendar it belongs to.
- `POST /api/v1/plans/{weekStart}/push-calendar` writing one deterministic-id event per day (summary "Dinner: {name}", household dinner_time for 60 minutes, description with the crock-pot "Start by" and a dinner-page link), insert-then-update on 409, recording `calendar_synced_at` and each day's `calendar_event_id` under the account lock only after every write succeeds.
- Token-failure handling: invalid_grant or a 401 after refresh on any Calendar call clears the token, sets `last_sync_error`, and yields 409 `google_reconnect_required` on push and `reconnect_required` from the new `GET /api/v1/integrations/google`; transient 429/5xx retry twice then surface a retryable 503 without disconnecting.
- The Settings → Google Calendar screen (connect link, calendar picker, Connected · {calendar}, Disconnect with confirmation, grape reconnect banner) and the plan screens' real Push to Calendar buttons with progress and error states.
## Integration checks at the feature head `081ad75fe256c99ae51082844242f3cd7e83be34` (5 commits ahead of `dev`, 0 behind)
- `gofmt -l .` — empty; `go vet ./...` — clean
- `go test -race -count=1 ./...` — 26 packages ok, 0 failures
- `CGO_ENABLED=0 go build ./...` — ok
- `make sqlc-check SQLC=$(go env GOPATH)/bin/sqlc` — no drift
- `cd web && npm run typecheck` — PASS; `npm test` — PASS 46 files / 804 tests; `npm run build` — PASS
- `make test`, `make build`, `docker build .` — PASS
The branch already contains all of `dev`, so no `dev` merge was needed.
## Deployment prerequisite — please read
`MEALQUEST_GOOGLE_CLIENT_ID` and `MEALQUEST_GOOGLE_CLIENT_SECRET_FILE` are **required at boot** (fail-fast, matching the OIDC variables). A release containing this feature will not start until the owner creates the Google OAuth web client and sets the secret on the host. The steps are in the runbook section "Google Calendar OAuth client", and `deploy/compose.yaml` / `deploy/.env.example` already carry the mount and variables.
## Notes
- Migration `0015_google_reconnect_required.sql` makes `integrations_google.refresh_token_ct` nullable with a CHECK that a row without a token carries a `last_sync_error`. The independent release-style review examined it and confirmed it is safe to apply to the existing household database at boot.
- Non-blocking findings were dispositioned and are tracked as Maestro task `d21e4a83` in Feature 22 (the same plan-screen hardening task, which the push mutation's retention fix has now partly closed).
- The real production push to the household "Dinners" calendar with a screenshot was split out into Maestro task `9a4d2601` in Feature 22 (owner-dependent: it needs the real OAuth client and the deployed service).
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Feature 16 — Google Calendar Sync
Integration pull request:
feature/c53328cb-577c-4aea-b4b2-817acfc3dfeb-google-calendar-sync->dev.One-way push of the weekly dinner schedule into a shared Google Calendar, with the app as source of truth (requirements §8 [Decided]; owner chose Google OAuth in Settings — D-011; CTXD RUNBOOKS "Google Cloud setup").
Tasks (all five merged, each independently reviewed at its exact head)
2cd2e5ad23d30efb7d8ddee67d523a1884dWhat the feature delivers
GET /api/v1/integrations/google/connect(session-bound state, PKCE,access_type=offline,prompt=consent), the callback exchanging the code with the file-sourced secret underMEALQUEST_PUBLIC_URL, the refresh token encrypted with secretbox, andDELETErevoking at Google and disconnecting. OnegrantedConnectiondecider reports not-connected / refused-grant / missing-calendar in a fixed order.GET .../calendars,PUT .../calendar) filtered to writer/owner, with oneforgetEventsinvalidation helper shared by a selection change and a disconnect, so no stored plan-day event id and nosyncedplan outlives the calendar it belongs to.POST /api/v1/plans/{weekStart}/push-calendarwriting one deterministic-id event per day (summary "Dinner: {name}", household dinner_time for 60 minutes, description with the crock-pot "Start by" and a dinner-page link), insert-then-update on 409, recordingcalendar_synced_atand each day'scalendar_event_idunder the account lock only after every write succeeds.last_sync_error, and yields 409google_reconnect_requiredon push andreconnect_requiredfrom the newGET /api/v1/integrations/google; transient 429/5xx retry twice then surface a retryable 503 without disconnecting.Integration checks at the feature head
081ad75fe256c99ae51082844242f3cd7e83be34(5 commits ahead ofdev, 0 behind)gofmt -l .— empty;go vet ./...— cleango test -race -count=1 ./...— 26 packages ok, 0 failuresCGO_ENABLED=0 go build ./...— okmake sqlc-check SQLC=$(go env GOPATH)/bin/sqlc— no driftcd web && npm run typecheck— PASS;npm test— PASS 46 files / 804 tests;npm run build— PASSmake test,make build,docker build .— PASSThe branch already contains all of
dev, so nodevmerge was needed.Deployment prerequisite — please read
MEALQUEST_GOOGLE_CLIENT_IDandMEALQUEST_GOOGLE_CLIENT_SECRET_FILEare required at boot (fail-fast, matching the OIDC variables). A release containing this feature will not start until the owner creates the Google OAuth web client and sets the secret on the host. The steps are in the runbook section "Google Calendar OAuth client", anddeploy/compose.yaml/deploy/.env.examplealready carry the mount and variables.Notes
0015_google_reconnect_required.sqlmakesintegrations_google.refresh_token_ctnullable with a CHECK that a row without a token carries alast_sync_error. The independent release-style review examined it and confirmed it is safe to apply to the existing household database at boot.d21e4a83in Feature 22 (the same plan-screen hardening task, which the push mutation's retention fix has now partly closed).9a4d2601in Feature 22 (owner-dependent: it needs the real OAuth client and the deployed service).Independent Codex gpt-6-sol review: APPROVED for this exact commit.