# JD Backend

Production backend for the JD Command Center + Wealth Engine lead capture. Replaces
the old localStorage-only Command Center prototype and the throwaway local Flask
dev server (`~/.hermes/skills/hermes-to-manus-wrapper/server.py`, port 8088).

FastAPI + Postgres (Vercel Postgres/Neon in production), no ORM. See
`/Users/skytrinidad/.claude/plans/twinkling-kindling-moler.md` for the original design
rationale (written when this targeted Render + SQLite — superseded by the Vercel +
Postgres deploy below, everything else in that plan still applies).

## Local dev

Needs a real Postgres to point at — SQLite was dropped once Vercel entered the
picture (serverless functions have no persistent disk). Easiest local option: use
the same Vercel Postgres instance production uses (get the connection string from
Vercel → Storage → your database → `.env.local` tab), or point at any Postgres
you have.

```bash
python3.11 -m venv .venv        # must be 3.11 or 3.12 — some deps have no 3.14 wheel yet
./.venv/bin/pip install -r requirements.txt

export DATABASE_URL='postgres://...'   # from Vercel Storage tab, or your own Postgres
ENV=dev ./.venv/bin/python scripts/seed_demo.py                          # loads demo leads/properties/content/tasks
ENV=dev ./.venv/bin/python scripts/seed_admin.py --username joseph --password 'pick-a-real-password'

ENV=dev SESSION_SECRET=dev-secret ./.venv/bin/uvicorn app.main:app --host 127.0.0.1 --port 8000
```

Open http://127.0.0.1:8000 — the Command Center UI (`static/index.html`) is served
from the same origin as the API, no CORS needed for the authenticated app.

## Deployed to Vercel

Project is git-linked to `github.com/skyrey1234-pixel/jd-denny-backend` — every push
to `main` deploys automatically. Remaining one-time setup, done from the Vercel
dashboard (project already exists, just needs these attached):

1. **Storage → Create Database → Postgres**, link it to this project. This
   auto-injects `POSTGRES_URL` (and a few variants) as environment variables —
   `app/db.py` reads `POSTGRES_URL` first, falling back to `DATABASE_URL`.
2. **Settings → Environment Variables**, add:
   - `SESSION_SECRET` — any long random string
   - `ALLOWED_ORIGINS` — comma-separated origins the funnel pages are served from
   - `SETUP_SECRET` — any long random string; gates the one-time `/api/setup/*`
     endpoints below. Safe to leave set (same posture as `MANYCHAT_WEBHOOK_SECRET`),
     or unset it after initial setup to close those routes off entirely.
   - `MANYCHAT_WEBHOOK_SECRET` — any long random string; required for the ManyChat
     Instagram DM webhook (`POST /api/public/leads/manychat`) to accept requests.
     Give this same value to ManyChat's External Request header (see
     `Joseph_Deliverables/Lead_Funnel/ManyChat_DM_Flow_Joseph_Denny.md`, section 5).
     Endpoint returns 501 until this is set.
3. Redeploy (or just wait for the next push) so the new env vars take effect.
4. Run the one-time setup calls against the live URL (no shell access needed on
   Vercel, hence these being HTTP endpoints instead of scripts):
   ```bash
   curl -X POST https://<your-deployment>/api/setup/init-admin \
     -H "X-Setup-Secret: <the SETUP_SECRET value>" -H "Content-Type: application/json" \
     -d '{"username":"joseph","password":"the-real-password"}'

   curl -X POST https://<your-deployment>/api/setup/seed-demo \
     -H "X-Setup-Secret: <the SETUP_SECRET value>"   # optional — demo starting data
   ```
5. Point a custom domain (e.g. `api.josephdennycbv.com`) at the project if desired
   (Vercel → Settings → Domains).
6. Update `JD_Wealth_Engine/config/brand.json` → `backend.lead_capture_endpoint` to
   the real URL (it currently holds a placeholder), then rerun
   `JD_Wealth_Engine/engine/run_cycle.py` to regenerate every funnel page with the
   correct endpoint baked in — **this step is required**, already-generated funnel
   pages won't pick up a config change on their own.

### Why Postgres instead of the original SQLite plan

Vercel functions are stateless serverless — no persistent disk, so a SQLite file
would silently reset on cold starts. `app/db.py` now wraps `psycopg2` in a thin
shim (`_ConnWrapper`) that mimics `sqlite3.Connection`'s `conn.execute(...).fetchone()`
convenience API and translates `?` placeholders to `%s`, so none of the router code
had to change query-by-query. `schema.sql` defines a `datetime(text)` SQL function
that always returns the current UTC timestamp, so every existing `datetime('now')`
call in the codebase keeps working verbatim instead of needing a find/replace to
`now()` across every file.

**Known limitation**: the in-memory rate limiter (`app/ratelimit.py`) resets per
cold start and isn't shared across concurrent invocations — it's hygiene, not a
real security boundary, same as documented before. Fine for a solo-agent pilot;
revisit (e.g. move counters into Postgres) if abuse ever becomes a real concern.

## What's verified so far

Against the SQLite version, locally, before the Postgres conversion:
- Login / session cookie / 401 → re-login
- Lead stage/owner/next-action edits, persisted and logged
- Property import (dry-run validation + commit), matching the original client-side
  `validateRecord`/`normalise` rules exactly
- "Make these into tasks" from a property's open questions
- Content approve/unapprove and inline body edits
- Task checkbox toggle
- Public funnel capture — tested against a real regenerated funnel page in a browser,
  including the auto-injected lead form's actual `fetch()` call
- ManyChat webhook (`/api/public/leads/manychat`) — secret-header auth, tag-to-display-text
  mapping, and the internal `/api/leads/capture-dm` endpoint it shares logic with (this
  also caught and fixed a real bug: the shared insert was missing the `gaps_json` binding,
  which would have 500'd every DM-lead capture, including from the Capture view UI)
- Restart persistence (SQLite file on disk survived a server restart)

After the Postgres conversion: all SQL reviewed by hand for `?`→`%s`/dialect
correctness (no local Postgres or Docker available in the dev sandbox to run it
live), app boots and degrades gracefully with a clear error when no database is
configured yet. **Not yet re-verified against a real Postgres** — that first real
test happens against the actual Vercel deployment once Storage is linked; run
through the checklist above (login, a lead edit, a property import, a funnel
capture) once that's done to confirm everything still works end-to-end.
