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 projectcreatortrack-499820). - What: Create branch
feat/food-ai-contacts-sync. Addhttps://www.googleapis.com/auth/contactsto the Google authorize scope set so the connect flow re-consents with the union (include_granted_scopesalready on). Enable the People API in the GCP project. - Verification:
git grep -n "auth/contacts" lib/google-connections.tsreturns 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 forgoogle_resource_name,google_etag,google_updatedon 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 ongetCrmData. - Verification:
npx tsc --noEmitpasses; 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 APIpeople.connections.list(personFields name/emailAddresses/phoneNumbers/organizations,requestSyncToken), page through, upsert into Personal Contacts nodes deduped byresourceName(fallback primary email, then normalized name). Persistetag+updated. Store the syncToken for incremental pulls. - Verification: dev API/one-liner runs
pullContactsagainst Justin's connected account; row count of Personal Contacts increases and each imported node has agoogle_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 noresourceName,people.createContact; for CT-edited people newer thangoogle_updated,people.updateContactwith storedetag. On 409/etag mismatch, re-fetch and apply last-writer-wins by timestamp. Write back the newresourceName/etag. - Verification: create a person in Personal Contacts (dev), run
pushContacts, confirm it appears in Google Contacts and the node gains aresourceName; 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(addop:"sync"→ pull then push; addop:"connect_status"), reuselib/google-connections.tsfor 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(mirrorcampaigns-sync/calendar timer), plus a small server sweep endpointop:"run_sync_sweep"inapp/api/apps/crm/route.tsthat 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 20shows 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.cssif 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-siteor 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). ReuseresolveProviderForUser, the base64/vision helpers fromapp/api/chat/route.ts, andlib/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,...}]}. Ifquestion(e.g. "calories left") → short text answer using passed-intotals+goal. Non-vision model on an image → clear error string. Never persists a meal. - Verification:
curlwith a test food photo file id returns parseableitems[]with macros;curlwithtext:"2 eggs and toast"returns items;curlwithquestionreturns 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 existingsetStagedFood→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, newcomponents/apps/meals/FoodChatBox.tsx,meals.css. ReuseChatThread/ChatContextpatterns. - 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 fromtotalsvs 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 --noEmitandnpm run buildclean. - 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, withURL:line),/home/justinwieb/forge/memory/general/reference_creatortrack_ai_food_logging.md(new, withURL:line),/home/justinwieb/.claude/projects/-home-justinwieb-forge/memory/MEMORY.md(index lines), then mergefeat/food-ai-contacts-sync→mainand runbash /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