Skip to content

Plan: CreatorTrack food AI + two-way Google Contacts sync

URL: https://mkdocs.justinsforge.com/memory/plans/creatortrack-food-contacts-ai-build-2026-07-08/

Date: 2026-07-08 Repo: /home/justinwieb/creatortrack (branch off main @ a2f8fe9) Approved spec: Three workstreams. WS1: two-way Google Contacts sync for the Personal Contacts app (id crm), reusing the Google OAuth + calendar-sync engine shape; People API read+write, dedup by resourceName, last-writer-wins via etag, a sync timer, and a Connect/Sync button. WS2: one shared Claude-vision endpoint that turns a food photo into structured macros and stages them into the existing Composer confirm flow. WS3: a compact AI chat box at the top of the food log (above the day counter) that describes-to-log via the same endpoint and answers "calories left today". Reuses existing rails: resolveProviderForUser + base64/vision pipeline (app/api/chat/route.ts), lib/image-downscale.ts, DayView's setStagedFood→Composer staged, and ChatThread patterns.

Out of scope (do NOT touch/redo): - Already shipped: Personal CRM→Personal Contacts rename, Composer persistent labels, TrendsView weekly dashboard, NutritionSheet Impact-on-Targets + Carb breakdown, barcode scanner + Open Food Facts. - Contacts groups/labels/starred, contact photos, multi-account Google contacts (single connection this round). - Auto-save of AI food (always confirm-in-Composer first).

Branch strategy: one feature branch feat/food-ai-contacts-sync, commit per task, single deploy_prod.sh at the very end (Task 12).

Task list

Task 1: Branch + contacts scope + People API enablement

  • Files: lib/google-connections.ts (scope list), docs/ note is not needed; GCP enablement is an ops step (People API in project creatortrack-499820).
  • What: Create branch feat/food-ai-contacts-sync. Add https://www.googleapis.com/auth/contacts to the Google authorize scope set so the connect flow re-consents with the union (include_granted_scopes already on). Enable the People API in the GCP project.
  • Verification: git grep -n "auth/contacts" lib/google-connections.ts returns the new scope; hit the connect authorize URL in a browser and confirm the consent screen lists "See, edit, download, and permanently delete your contacts".
  • Commit: feat(contacts): request People API contacts scope on Google connect

Task 2: Contact-node sync metadata helpers

  • Files: lib/crm.ts (add read/write helpers for google_resource_name, google_etag, google_updated on a person node's collectionValues).
  • What: Extend the person model so each contact can carry Google sync keys stored as collectionValues (no SQL migration; generic node storage). Add setGoogleSyncMeta(personId, {resourceName, etag, updated}) and expose the fields on getCrmData.
  • Verification: npx tsc --noEmit passes; a Node one-liner (or dev API call) sets and reads back the sync meta on a test person.
  • Commit: feat(contacts): store google resourceName/etag/updated on person nodes

Task 3: Pull engine (Google → CT)

  • Files: lib/google-contacts.ts (new).
  • What: pullContacts(workspaceId): People API people.connections.list (personFields name/emailAddresses/phoneNumbers/organizations, requestSyncToken), page through, upsert into Personal Contacts nodes deduped by resourceName (fallback primary email, then normalized name). Persist etag+updated. Store the syncToken for incremental pulls.
  • Verification: dev API/one-liner runs pullContacts against Justin's connected account; row count of Personal Contacts increases and each imported node has a google_resource_name.
  • Commit: feat(contacts): pull Google contacts into Personal Contacts (dedup by resourceName)

Task 4: Push engine (CT → Google) + conflict resolution

  • Files: lib/google-contacts.ts.
  • What: pushContacts(workspaceId): for CT-created people with no resourceName, people.createContact; for CT-edited people newer than google_updated, people.updateContact with stored etag. On 409/etag mismatch, re-fetch and apply last-writer-wins by timestamp. Write back the new resourceName/etag.
  • Verification: create a person in Personal Contacts (dev), run pushContacts, confirm it appears in Google Contacts and the node gains a resourceName; edit on both sides and confirm newer edit wins.
  • Commit: feat(contacts): push CT contacts to Google with etag last-writer-wins

Task 5: Sync op in the CRM API route

  • Files: app/api/apps/crm/route.ts (add op:"sync" → pull then push; add op:"connect_status"), reuse lib/google-connections.ts for connection lookup.
  • What: Wire an op:"sync" that runs pull+push for the caller's workspace and returns counts; op:"connect_status" reports whether a Google connection with contacts scope exists.
  • Verification: curl -fsS -X POST localhost:<devport>/api/apps/crm -d '{"op":"sync"}' -H 'cookie: <dev auth>' returns {ok:true, pulled, pushed}.
  • Commit: feat(contacts): op:sync (pull+push) on the Personal Contacts API

Task 6: Sync timer (systemd)

  • Files: /etc/systemd/system/creatortrack-contacts-sync.service + .timer (mirror campaigns-sync/calendar timer), plus a small server sweep endpoint op:"run_sync_sweep" in app/api/apps/crm/route.ts that iterates connected workspaces.
  • What: Periodic (e.g. every 30 min) two-way sync for all workspaces with a contacts-scoped Google connection.
  • Verification: systemctl start creatortrack-contacts-sync.service && journalctl -u creatortrack-contacts-sync -n 20 shows a clean pull+push; systemctl list-timers | grep contacts-sync.
  • Commit: feat(contacts): contacts-sync systemd timer (30m two-way sweep)

Task 7: Connect/Sync button in the UI

  • Files: components/apps/crm/CrmClient.tsx (+ crm.css if present).
  • What: Header button: "Connect Google" when no contacts-scoped connection (links to the Google connect flow with contacts scope), else "Sync now" (calls op:"sync", shows spinner + last-synced + counts toast). Failure → toast, no partial UI state.
  • Verification: /preview-site or dev CT: button reflects connect status, "Sync now" pulls/pushes and toasts counts.
  • Commit: feat(contacts): Connect/Sync Google button in Personal Contacts

Task 8: Shared AI-food vision endpoint

  • Files: app/api/apps/meals/ai-food/route.ts (new). Reuse resolveProviderForUser, the base64/vision helpers from app/api/chat/route.ts, and lib/image-downscale.ts.
  • What: POST accepts {image?: fileId, text?: string, question?: string}. If image/text describes food → one Claude vision/text call returning strict JSON {items:[{name,grams,kcal,protein,carbs,fat,fiber,sugar,...}]}. If question (e.g. "calories left") → short text answer using passed-in totals+goal. Non-vision model on an image → clear error string. Never persists a meal.
  • Verification: curl with a test food photo file id returns parseable items[] with macros; curl with text:"2 eggs and toast" returns items; curl with question returns a sentence.
  • Commit: feat(meals): shared Claude AI-food endpoint (photo/text -> macros JSON)

Task 9: Camera/upload button in the food log

  • Files: components/apps/meals/DayView.tsx (+ meals.css).
  • What: Add a camera/upload icon button (next to the barcode button) that uploads a photo, calls /api/apps/meals/ai-food, and stages the returned item(s) via existing setStagedFood→Composer confirm. Multi-item results stage sequentially or as a pick-list.
  • Verification: dev CT: snap/upload a plate photo, Composer opens pre-filled with detected food + macros for confirm-and-Add.
  • Commit: feat(meals): AI food-photo button -> stage into Composer

Task 10: Food-log AI chat box (top of DayView)

  • Files: components/apps/meals/DayView.tsx, new components/apps/meals/FoodChatBox.tsx, meals.css. Reuse ChatThread/ChatContext patterns.
  • What: Compact chat box rendered above the calories summary card (line ~274). Text/voice input → /api/apps/meals/ai-food; food descriptions stage into Composer, questions ("how many calories left") answer inline from totals vs goal.
  • Verification: dev CT: type "a banana", Composer stages a banana; type "calories left today", box answers with remaining vs calGoal.
  • Commit: feat(meals): AI chat box above the day counter (describe-to-log + Q&A)

Task 11: End-to-end verification pass

  • Files: none (test only).
  • What: Drive all three workstreams on dev CT with real data: connect Google + two-way contact round-trip, photo log, chat log, chat Q&A. Fix any breakage found.
  • Verification: manual E2E checklist all green on dev; npx tsc --noEmit and npm run build clean.
  • Commit: test(ct): e2e verification for food AI + contacts sync (only if fixes land)

Task 12: Register, document, deploy

  • Files: /home/justinwieb/forge/memory/general/reference_creatortrack_contacts_sync.md (new, with URL: line), /home/justinwieb/forge/memory/general/reference_creatortrack_ai_food_logging.md (new, with URL: line), /home/justinwieb/.claude/projects/-home-justinwieb-forge/memory/MEMORY.md (index lines), then merge feat/food-ai-contacts-syncmain and run bash /home/justinwieb/creatortrack/scripts/deploy_prod.sh.
  • What: Document both new subsystems (contacts sync + AI food), add MEMORY.md index entries, merge, single prod deploy.
  • Verification: curl -fsS https://app.creatortrack.ai/... health OK post-deploy; Personal Contacts "Sync now" works on prod; AI photo + chat box work on prod.
  • Commit: docs(ct): register contacts-sync + AI-food; deploy to prod