Skip to content

Plan: Sales CRM Phase B, Gmail ingest + auto last-touch + cadence follow-ups

URL: https://mkdocs.justinsforge.com/memory/plans/sales-crm-phase-b-gmail-ingest-2026-07-06/

Date: 2026-07-06 Approved spec: Business-Gmail-only ingest job that matches messages against crm.contacts.emails[], upserts them as crm.activities rows (kind=email, subject + snippet stored, full body loaded live from Gmail on demand, never stored), advances last_touch_at on any matched email in either direction (never backwards). Trigger = token-gated jobs route (finance-jobs pattern, SALES_JOBS_TOKEN) + user-scope systemd timer every 30 min (written, enabled only at merge) + in-app "Sync now". Reminders in-app only: Follow-ups overdue strip (now - last_touch_at > cadence_days) with quick log-touch. Watermark state in crm.sync_state. Errors surface in job result, per-chunk failures don't kill the run.

Out of scope: - Personal Gmail account - Telegram/morning-report digests - Auto-creating contacts from unknown senders - Contracts, tasks linkage, ERPNext invoice link (Phase C) - Outreach draft agent, LinkedIn anything (Phase D) - Enabling the systemd timer (merge-time step)

Conventions: worktree <WT> = /home/justinwieb/forge-suite-wt/feat/sales-crm, branch feat/sales-crm, dev slot :3064, DB clone ct_feat_sales_crm (on forge-data; reach via app API or forge_workspace_migrate.py). Token bridge reuses lib/gmail.ts getAccessToken("business") seam; NEVER hand-roll auth.

Task list

Task 1: Migration, email-activity columns + sync watermark

  • Files: <WT>/db/migrations/<stamp>_sales_crm_email_ingest.sql (via forge_workspace_new_migration.py)
  • What: alter crm.activities add gmail_message_id text, gmail_thread_id text, direction text check (direction in ('in','out')), source_account text; partial unique index on (workspace_id, gmail_message_id) where gmail_message_id is not null; new table crm.sync_state (workspace_id, job_key, last_synced_at, detail jsonb; unique(workspace_id, job_key)) with RLS + grants matching Phase A tables.
  • Verification: (cd <WT> && PGDATABASE=ct_feat_sales_crm WORKSPACE_MIGRATIONS_DIR=db/migrations python3 ~/forge/scripts/forge_workspace_migrate.py) then \d crm.activities shows the 4 columns and \dt crm.sync_state exists
  • Commit: feat(sales-crm): email-activity columns + crm.sync_state watermark

Task 2: Gmail ingest job

  • Files: <WT>/lib/sales-crm-jobs/gmail.ts, small export additions to <WT>/lib/sales-crm.ts
  • What: runGmailSync(workspaceId): load contact emails, chunk ~15 addresses per Gmail query (from: OR to: each addr, newer_than/after: from watermark minus 1d overlap), business account only via the lib/gmail.ts token seam, classify direction by whether from_addr is a contact email, upsert activities on gmail_message_id conflict do nothing, advance last_touch_at = greatest(last_touch_at, msg date), write watermark. Per-chunk try/catch collects errors into the result; throws only if every chunk fails.
  • Verification: (cd <WT> && npx tsc --noEmit)
  • Commit: feat(sales-crm): business-Gmail ingest job (activities + auto last-touch)

Task 3: Jobs route + in-app sync op

  • Files: <WT>/app/api/apps/sales/jobs/route.ts, <WT>/app/api/apps/sales/route.ts (add op: "gmail_sync"), <WT>/.env.local (dev SALES_JOBS_TOKEN)
  • What: Bearer-token route (compare to SALES_JOBS_TOKEN, 401 otherwise; finance-jobs pattern) that runs runGmailSync for all workspaces with the sales app installed; session-authed op: "gmail_sync" in the main route for the in-app button (single workspace).
  • Verification: token curl returns job summary JSON; no-token curl returns 401; npx tsc --noEmit
  • Commit: feat(sales-crm): token-gated gmail-sync jobs route + in-app sync op

Task 4: Timeline email rendering + on-demand full body

  • Files: <WT>/app/api/apps/sales/route.ts (GET ?messageBody=<id>), <WT>/lib/gmail.ts or job lib (body fetch by message id, business account), <WT>/components/apps/sales/ContactsView.tsx, <WT>/components/apps/sales/sales.css
  • What: Email activities render subject + snippet + direction arrow + account; "Load full message" expander fetches body live by gmail_message_id (text/plain part, fallback stripped HTML); body never persisted.
  • Verification: dev-slot click-through: contact timeline shows synced email, expander loads body; npx next build --no-lint tail clean
  • Commit: feat(sales-crm): email activities in timeline with on-demand full body

Task 5: Follow-ups overdue strip + Sync now

  • Files: <WT>/components/apps/sales/SalesClient.tsx, <WT>/components/apps/sales/FollowUpsStrip.tsx (new), <WT>/components/apps/sales/sales.css
  • What: Strip above tabs listing contacts where now - last_touch_at > cadence_days (or never touched with cadence set), sorted most-overdue-first, each with "Log touch" (posts a touch activity, which advances last_touch) and click-through to contact panel. "Sync email" button + last-sync stamp from crm.sync_state.
  • Verification: dev slot: set a contact cadence_days=1, backdate last_touch via update op, strip shows it; Log touch clears it
  • Commit: feat(sales-crm): follow-ups overdue strip + in-app email sync

Task 6: systemd units (written, not enabled)

  • Files: /home/justinwieb/forge/infra/systemd/forge-creatortrack-sales-gmail-sync.service, .timer (30 min, user scope), note in topic file
  • What: Curl-the-jobs-route units mirroring forge-creatortrack-campaigns-sync.*; NOT installed/enabled until merge (prod token + prod URL at that point).
  • Verification: systemd-analyze verify on both unit files passes (paths only; not enabled)
  • Commit: feat(sales-crm): gmail-sync systemd units (enable at merge) (forge repo)

Task 7: Register and document

  • Files: /home/justinwieb/forge/memory/general/reference_creatortrack_sales_crm.md (Phase B section), MEMORY.md index line update, this plan's outcome section
  • What: Document ingest architecture, token env var, timer enable step for merge day, what Phases C-D still owe. Run bash /home/justinwieb/forge/scripts/forge_eval_run.sh.
  • Verification: eval run passes
  • Commit: docs(sales-crm): register Phase B in memory + topic file (forge repo)

Outcome (2026-07-06)

All 7 tasks complete, same session as approval. CreatorTrack branch feat/sales-crm gained 5 commits (007e9ab migration, f7fa56c ingest job, 9648d69 routes, 871bd38 timeline+body, 66fdbd3 follow-ups strip); forge repo gained the systemd units + trigger script (6b67207). Verified live on :3064 against real business Gmail: probe contact ([email protected]) matched 127 messages, 124 activities inserted, idempotent re-run inserted 0, on-demand body loads, 400/401/404 guards all tested, trigger-script dry-run 200. SALES_JOBS_TOKEN registered in ~/.forge-secrets/creatortrack-auth.env so merge day is just: merge, restart prod with the env, systemctl --user enable --now forge-creatortrack-sales-gmail-sync.timer. Deviations: none of substance; strip list got a 6-row max-height after visual QA. Probe contact ZZTest (id 22) left in the dev clone for click-through.