Push plan to Google Calendar (Feature 16) #127

Member

Task

Maestro task 5885d2b1-cf06-44ff-abf3-242d7763c3c9 — "Push plan to Google Calendar" (Feature 16, task 3 of 5).

Done-condition

POST /api/v1/plans/{weekStart}/push-calendar writes one event per plan day to the selected calendar: summary "Dinner: {name}", start at household dinner_time and end 60 min later in the household timezone, description with the dinner name, "Start by {time}" for crock-pot dinners and a link to {MEALQUEST_PUBLIC_URL}/dinner/{id}. Event ids are deterministic (lowercase base32hex of SHA-256 of "mealquest|{weekStart}|{date}", within Google's id rules); insert, and on 409 update the existing event. Afterwards the plan is synced with calendar_synced_at and plan_days.calendar_event_id is stored. Feature 17 owns generating the week's shopping list from a pushed plan — this task exposes what it needs and does not stub it.

Scope

The push endpoint, the event id/times/description, the synced bookkeeping, the Feature 17 seam and tests. Task b8d8da5a's invalid_grant disconnect bookkeeping, the settings screen (677261bc) and any shopping-list generation are untouched. No migration: the schema already has calendar_synced_at and calendar_event_id.

Points for review

  • The deterministic event id satisfies Google's charset and length rules, insert-versus-409-update behaves as specified, and a second push truly creates no new events.
  • The times are the household dinner_time in the household timezone with the UTC instant shifting correctly across a DST transition in the week (the developer tested both fall-back 2026-10-26 and spring-forward 2027-03-08).
  • A partial failure must not leave the plan marked synced; the developer states what it chose.
  • The stale-selection hand-off from task 0fe3f1f2: a selected calendar since deleted or no longer writable is reported clearly rather than failing obscurely, and a 403 requiredAccessLevel is mapped rather than swallowed.
  • The Feature 17 seam is exposed and nothing more — no shopping-list code, no stub.
  • internal/gcal/calendars.go was refactored (the connection read split out of accessToken for reuse) and internal/suggest/suggest_test.go was touched: check both for regressions.
  • migrations/queries/plans.sql gained queries and sqlc was regenerated (committed, no drift); applied migrations untouched.

Evidence (orchestrator-run gate at 691c1ff34a)

  • 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 && npm test && npm run build — PASS, 45 files / 764 tests (no web change)

The developer ran 13 deliberate mutation checks (no update on 409, always push all days, swap keeps the event id, no revision check, fixed-offset times, marked synced despite a failure, no-calendar check removed, 403 unmapped, Google's body leaked, event-id prefix changed, no "Start by" line, account lock removed, no no_calendar mapping in the server) and reports all 13 were caught. The endpoint was also exercised through the real router against the fake Google; it was not run against real Google (no credentials or network).

Implementation commit: 691c1ff34abfd1ee182053ad0067077a690ac148

## Task Maestro task `5885d2b1-cf06-44ff-abf3-242d7763c3c9` — "Push plan to Google Calendar" (Feature 16, task 3 of 5). ## Done-condition `POST /api/v1/plans/{weekStart}/push-calendar` writes one event per plan day to the selected calendar: summary "Dinner: {name}", start at household dinner_time and end 60 min later in the household timezone, description with the dinner name, "Start by {time}" for crock-pot dinners and a link to `{MEALQUEST_PUBLIC_URL}/dinner/{id}`. Event ids are deterministic (lowercase base32hex of SHA-256 of "mealquest|{weekStart}|{date}", within Google's id rules); insert, and on 409 update the existing event. Afterwards the plan is synced with `calendar_synced_at` and `plan_days.calendar_event_id` is stored. Feature 17 owns generating the week's shopping list from a pushed plan — this task exposes what it needs and does not stub it. ## Scope The push endpoint, the event id/times/description, the synced bookkeeping, the Feature 17 seam and tests. Task `b8d8da5a`'s invalid_grant disconnect bookkeeping, the settings screen (`677261bc`) and any shopping-list generation are untouched. No migration: the schema already has `calendar_synced_at` and `calendar_event_id`. ## Points for review - The deterministic event id satisfies Google's charset and length rules, insert-versus-409-update behaves as specified, and a second push truly creates no new events. - The times are the household dinner_time in the household timezone with the UTC instant shifting correctly across a DST transition in the week (the developer tested both fall-back 2026-10-26 and spring-forward 2027-03-08). - A partial failure must not leave the plan marked synced; the developer states what it chose. - The stale-selection hand-off from task `0fe3f1f2`: a selected calendar since deleted or no longer writable is reported clearly rather than failing obscurely, and a 403 `requiredAccessLevel` is mapped rather than swallowed. - The Feature 17 seam is exposed and nothing more — no shopping-list code, no stub. - `internal/gcal/calendars.go` was refactored (the connection read split out of `accessToken` for reuse) and `internal/suggest/suggest_test.go` was touched: check both for regressions. - `migrations/queries/plans.sql` gained queries and sqlc was regenerated (committed, no drift); applied migrations untouched. ## Evidence (orchestrator-run gate at 691c1ff34abfd1ee182053ad0067077a690ac148) - `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 && npm test && npm run build` — PASS, 45 files / 764 tests (no web change) The developer ran 13 deliberate mutation checks (no update on 409, always push all days, swap keeps the event id, no revision check, fixed-offset times, marked synced despite a failure, no-calendar check removed, 403 unmapped, Google's body leaked, event-id prefix changed, no "Start by" line, account lock removed, no `no_calendar` mapping in the server) and reports all 13 were caught. The endpoint was also exercised through the real router against the fake Google; it was not run against real Google (no credentials or network). Implementation commit: `691c1ff34abfd1ee182053ad0067077a690ac148`
orchestrator-agent added 1 commit 2026-10-07 22:23:44 -04:00
Push the week's plan to Google Calendar (Feature 16)
CI / ci (push) Successful in 3m52s
CI / ci (pull_request) Successful in 3m52s
691c1ff34a
POST /api/v1/plans/{weekStart}/push-calendar writes one event per plan
day to the chosen calendar: "Dinner: {name}" from the household dinner
time for 60 minutes in the household zone, with the dinner name,
"Start by {time}" for crock-pot dinners and the
{MEALQUEST_PUBLIC_URL}/dinner/{id} link. Event ids are the lowercase
base32hex SHA-256 of "mealquest|{weekStart}|{date}"; each event is
inserted, and a 409 updates it instead.

gcal.Service.PushEvents does the Calendar writes under the account lock,
with the same redaction as listing. plan.Service.Push records a push only
when every event was written: the plan becomes synced with
calendar_synced_at and each day stores its event id. A partial failure
records nothing; a plan edited mid-push is 409 plan_changed.

A swap, or a redraft to another dinner, clears the day's
calendar_event_id, so the next push writes just those days; a push with
none pending writes every day again.

No calendar chosen is 409 no_calendar; a deleted or read-only chosen
calendar is 409 calendar_unavailable; a refused grant is 502
reconnect_required, leaving the connection as it is.

Maestro task 5885d2b1-cf06-44ff-abf3-242d7763c3c9.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
review_agent requested changes 2026-10-07 22:25:21 -04:00
Dismissed
review_agent left a comment
Member

Independent Codex gpt-6-sol review: CHANGES_REQUIRED for this exact commit.

Independent Codex gpt-6-sol review: CHANGES_REQUIRED for this exact commit.
overseer added 1 commit 2026-10-07 22:42:28 -04:00
Forget pushed event ids when another calendar is chosen (Feature 16)
CI / ci (push) Successful in 4m10s
CI / ci (pull_request) Successful in 4m25s
26fde27f54
Choosing a calendar other than the stored one now clears every plan
day's calendar_event_id and edits each plan that had one (revision
raised, synced -> changed) in the same transaction as the choice, so a
stored event id is always on the chosen calendar and the next push
writes the whole week there.

PushEvents now runs the push's record under the same account-lock hold
as its event writes, so the chosen calendar cannot change between the
writes and the sync bookkeeping; a push that read a plan before the
choice is refused by its revision check (ErrPlanChanged).

Fixes review finding F-01 on task 5885d2b1-cf06-44ff-abf3-242d7763c3c9.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
review_agent requested changes 2026-10-07 22:52:13 -04:00
Dismissed
review_agent left a comment
Member

Independent Codex gpt-6-sol review: CHANGES_REQUIRED for this exact commit.

Independent Codex gpt-6-sol review: CHANGES_REQUIRED for this exact commit.
overseer added 1 commit 2026-10-07 23:07:40 -04:00
Forget pushed event ids when Google Calendar is disconnected (Feature 16)
CI / ci (pull_request) Successful in 4m33s
CI / ci (push) Successful in 3m49s
fb7d8ddff8
Disconnect deleted the connection but left every pushed plan synced with
its stored event ids, though no calendar was chosen any more. It now
deletes the connection and runs the same invalidation as choosing another
calendar (forgetEvents: MarkCalendarPlansEdited, ClearCalendarEvents) in
one transaction, under the account lock. A refused revoke still changes
nothing.

Tests push three weeks, disconnect, and check each is changed with no
event id; then connect again, choose a calendar and push the whole week
to it. Also: a refused revoke keeps every week synced, and a disconnect
during a push waits for its record and then forgets it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
review_agent left a comment
Member

Independent Codex gpt-6-sol review: APPROVED for this exact commit.

Independent Codex gpt-6-sol review: APPROVED for this exact commit.
approver_agent merged commit 6262489376 into feature/c53328cb-577c-4aea-b4b2-817acfc3dfeb-google-calendar-sync 2026-10-07 23:27:47 -04:00
Sign in to join this conversation.
No Reviewers
No labels
3 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: Maniacs_Mansion/MealQuest#127